From b52d39c3b7e6c9e60781b94d04ba1b56c7c22739 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Sat, 20 Apr 2019 13:36:26 +0200 Subject: [PATCH 01/10] feat(integration): Add Falcon integration --- sentry_sdk/integrations/falcon.py | 162 ++++++++++++ setup.py | 1 + tests/integrations/falcon/test_falcon.py | 317 +++++++++++++++++++++++ tox.ini | 7 + 4 files changed, 487 insertions(+) create mode 100644 sentry_sdk/integrations/falcon.py create mode 100644 tests/integrations/falcon/test_falcon.py diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py new file mode 100644 index 0000000000..105fbbcb2b --- /dev/null +++ b/sentry_sdk/integrations/falcon.py @@ -0,0 +1,162 @@ +from typing import Any, Callable, Dict + +import falcon +import falcon.api_helpers +import sentry_sdk.integrations +from sentry_sdk.hub import Hub +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception + + +class FalconRequestExtractor(RequestExtractor): + + def env(self): + return self.request.env + + def cookies(self): + return self.request.cookies + + def raw_data(self): + # As request data can only read once we won't make this available + # to Sentry. + # TODO(jmagnusson): Figure out if there's a way to support this + return None + + def form(self): + return None # No such concept in Falcon + + def files(self): + return None # No such concept in Falcon + + def json(self): + # We don't touch falcon.Request.media as that can raise an exception + # on non-JSON requests. + return self.request._media + + +class SentryFalconMiddleware(object): + """Captures exceptions in Falcon requests and send to Sentry""" + + def process_request(self, req, resp, *args, **kwargs): + hub = Hub.current + integration = hub.get_integration(FalconIntegration) + if integration is None: + return + + with hub.configure_scope() as scope: + scope.add_event_processor( + _make_request_event_processor(req, integration) + ) + + +class FalconIntegration(sentry_sdk.integrations.Integration): + identifier = "falcon" + + transaction_style = None + + def __init__(self, transaction_style="uri_template"): + # type: (str) -> None + TRANSACTION_STYLE_VALUES = ("uri_template", "path") + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once(): + # type: () -> None + _patch_wsgi_app() + _patch_handle_exception() + _patch_prepare_middleware() + + +def _patch_wsgi_app(): + original_wsgi_app = falcon.API.__call__ + + def sentry_patched_wsgi_app(self, env, start_response): + hub = Hub.current + integration = hub.get_integration(FalconIntegration) + if integration is None: + return original_wsgi_app(self, env, start_response) + + sentry_wrapped = SentryWsgiMiddleware( + lambda envi, start_resp: original_wsgi_app(self, envi, start_resp) + ) + with hub.push_scope() as scope: + scope._name = "falcon" + resp = sentry_wrapped(env, start_response) + + return resp + + falcon.API.__call__ = sentry_patched_wsgi_app + + +def _patch_handle_exception(): + original_handle_exception = falcon.API._handle_exception + + def sentry_patched_handle_exception(self, *args): + # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception + # method signature from `(ex, req, resp, params)` to + # `(req, resp, ex, params)` + if isinstance(args[0], Exception): + ex = args[0] + else: + ex = args[2] + + was_handled = original_handle_exception(self, *args) + + hub = Hub.current + integration = hub.get_integration(FalconIntegration) + + if integration is not None and not was_handled: + event, hint = event_from_exception( + ex, + client_options=hub.client.options, + mechanism={"type": "falcon", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + return was_handled + + falcon.API._handle_exception = sentry_patched_handle_exception + + +def _patch_prepare_middleware(): + original_prepare_middleware = falcon.api_helpers.prepare_middleware + + def sentry_patched_prepare_middleware( + middleware=None, + independent_middleware=False + ): + hub = Hub.current + integration = hub.get_integration(FalconIntegration) + if integration is not None: + middleware = [SentryFalconMiddleware()] + (middleware or []) + return original_prepare_middleware( + middleware, + independent_middleware, + ) + + falcon.api_helpers.prepare_middleware = \ + sentry_patched_prepare_middleware + + +def _make_request_event_processor(req, integration): + # type: (falcon.Request, FalconIntegration) -> Callable + + def inner(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + if integration.transaction_style == "uri_template": + event["transaction"] = req.uri_template + elif integration.transaction_style == "path": + event["transaction"] = req.path + + with capture_internal_exceptions(): + FalconRequestExtractor(req).extract_into_event(event) + + return event + + return inner diff --git a/setup.py b/setup.py index e2ae7e7654..f036b38450 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ extras_require={ "flask": ["flask>=0.8", "blinker>=1.1"], "bottle": ["bottle>=0.12.13"], + "falcon": ["falcon>=1.4"], }, classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/tests/integrations/falcon/test_falcon.py b/tests/integrations/falcon/test_falcon.py new file mode 100644 index 0000000000..3a22138183 --- /dev/null +++ b/tests/integrations/falcon/test_falcon.py @@ -0,0 +1,317 @@ +import logging + +import pytest + +pytest.importorskip("falcon") + +import falcon +import falcon.testing +import sentry_sdk +import sentry_sdk.integrations.falcon as falcon_sentry +from sentry_sdk.integrations.logging import LoggingIntegration + + +@pytest.fixture +def make_app(sentry_init): + def inner(): + class MessageResource: + def on_get(self, req, resp): + sentry_sdk.capture_message('hi') + resp.media = 'hi' + + app = falcon.API() + app.add_route('/message', MessageResource()) + + return app + return inner + + +@pytest.fixture +def make_client(make_app): + def inner(): + app = make_app() + return falcon.testing.TestClient(app) + return inner + + +def test_has_context(sentry_init, capture_events, make_client): + sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + events = capture_events() + + client = make_client() + response = client.simulate_get("/message") + assert response.status == falcon.HTTP_200 + + event, = events + assert event["transaction"] == "/message" # Falcon URI template + assert "data" not in event["request"] + assert event["request"]["url"] == "http://falconframework.org/message" + + +@pytest.mark.parametrize( + "transaction_style,expected_transaction", + [("uri_template", "/message"), ("path", "/message")] +) +def test_transaction_style( + sentry_init, make_client, capture_events, transaction_style, expected_transaction +): + integration = falcon_sentry.FalconIntegration( + transaction_style=transaction_style + ) + sentry_init(integrations=[integration]) + events = capture_events() + + client = make_client() + response = client.simulate_get("/message") + assert response.status == falcon.HTTP_200 + + event, = events + assert event["transaction"] == expected_transaction + + +def test_errors(sentry_init, capture_exceptions, capture_events): + sentry_init(integrations=[falcon_sentry.FalconIntegration()], debug=True) + + class ZeroDivisionErrorResource: + def on_get(self, req, resp): + 1 / 0 + + app = falcon.API() + app.add_route("/", ZeroDivisionErrorResource()) + + exceptions = capture_exceptions() + events = capture_events() + + client = falcon.testing.TestClient(app) + + try: + client.simulate_get("/") + except ZeroDivisionError: + pass + + exc, = exceptions + assert isinstance(exc, ZeroDivisionError) + + event, = events + assert event["exception"]["values"][0]["mechanism"]["type"] == "falcon" + + +def test_falcon_large_json_request(sentry_init, capture_events): + sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + + data = {"foo": {"bar": "a" * 2000}} + + class Resource: + def on_post(self, req, resp): + assert req.media == data + sentry_sdk.capture_message("hi") + resp.media = "ok" + + app = falcon.API() + app.add_route("/", Resource()) + + events = capture_events() + + client = falcon.testing.TestClient(app) + response = client.simulate_post("/", json=data) + assert response.status == falcon.HTTP_200 + + event, = events + assert event["_meta"]["request"]["data"]["foo"]["bar"] == { + "": {"len": 2000, "rem": [["!limit", "x", 509, 512]]} + } + assert len(event["request"]["data"]["foo"]["bar"]) == 512 + + +@pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"]) +def test_falcon_empty_json_request(sentry_init, capture_events, data): + sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + + class Resource: + def on_post(self, req, resp): + assert req.media == data + sentry_sdk.capture_message("hi") + resp.media = "ok" + + app = falcon.API() + app.add_route("/", Resource()) + + events = capture_events() + + client = falcon.testing.TestClient(app) + response = client.simulate_post("/", json=data) + assert response.status == falcon.HTTP_200 + + event, = events + assert event["request"]["data"] == data + + +def test_logging(sentry_init, capture_events): + sentry_init( + integrations=[ + falcon_sentry.FalconIntegration(), + LoggingIntegration(event_level="ERROR"), + ] + ) + + logger = logging.getLogger() + + app = falcon.API() + + class Resource: + def on_get(self, req, resp): + logger.error("hi") + resp.media = "ok" + + app.add_route("/", Resource()) + + events = capture_events() + + client = falcon.testing.TestClient(app) + client.simulate_get("/") + + event, = events + assert event["level"] == "error" + + +def test_500(sentry_init, capture_events): + sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + + app = falcon.API() + + class Resource: + def on_get(self, req, resp): + 1 / 0 + + app.add_route("/", Resource()) + + def http500_handler(ex, req, resp, params): + sentry_sdk.capture_exception(ex) + resp.media = { + "message": "Sentry error: %s" % sentry_sdk.last_event_id() + } + + app.add_error_handler(Exception, http500_handler) + + events = capture_events() + + client = falcon.testing.TestClient(app) + response = client.simulate_get("/") + + event, = events + assert response.json == {"message": "Sentry error: %s" % event["event_id"]} + + +def test_error_in_errorhandler(sentry_init, capture_events): + sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + + app = falcon.API() + + class Resource: + def on_get(self, req, resp): + raise ValueError() + + app.add_route("/", Resource()) + + def http500_handler(ex, req, resp, params): + sentry_sdk.capture_exception(ex) + 1 / 0 + + app.add_error_handler(Exception, http500_handler) + + events = capture_events() + + client = falcon.testing.TestClient(app) + + with pytest.raises(ZeroDivisionError): + client.simulate_get("/") + + event1, event2 = events + + exception, = event1["exception"]["values"] + assert exception["type"] == "ValueError" + + exception = event2["exception"]["values"][-1] + assert exception["type"] == "ZeroDivisionError" + + +def test_bad_request_not_captured(sentry_init, capture_events): + sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + events = capture_events() + + app = falcon.API() + + class Resource: + def on_get(self, req, resp): + raise falcon.HTTPBadRequest() + + app.add_route("/", Resource()) + + client = falcon.testing.TestClient(app) + + client.simulate_get("/") + + assert not events + + +def test_does_not_leak_scope(sentry_init, capture_events): + sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + events = capture_events() + + with sentry_sdk.configure_scope() as scope: + scope.set_tag("request_data", False) + + app = falcon.API() + + class Resource: + def on_get(self, req, resp): + with sentry_sdk.configure_scope() as scope: + scope.set_tag("request_data", True) + + def generator(): + for row in range(1000): + with sentry_sdk.configure_scope() as scope: + assert scope._tags["request_data"] + + yield (str(row) + "\n").encode() + + resp.stream = generator() + + app.add_route("/", Resource()) + + client = falcon.testing.TestClient(app) + response = client.simulate_get("/") + + expected_response = "".join(str(row) + "\n" for row in range(1000)) + assert response.text == expected_response + assert not events + + with sentry_sdk.configure_scope() as scope: + assert not scope._tags["request_data"] + + +@pytest.mark.parametrize("exc_cls", [ZeroDivisionError, Exception]) +def test_errorhandler_for_exception_swallows_exception( + sentry_init, capture_events, exc_cls +): + sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + events = capture_events() + + app = falcon.API() + + class Resource: + def on_get(self, req, resp): + 1 / 0 + + app.add_route("/", Resource()) + + def exc_handler(ex, req, resp, params): + resp.media = "ok" + + app.add_error_handler(exc_cls, exc_handler) + + client = falcon.testing.TestClient(app) + + response = client.simulate_get("/") + assert response.status == falcon.HTTP_200 + assert not events diff --git a/tox.ini b/tox.ini index 916ff6ecb1..76b72ab5ee 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,8 @@ envlist = {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-bottle-0.12 + {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-falcon-1.4 + {py3.5,py3.6,py3.7}-sanic-{0.8,18,19} {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-celery-{4.1,4.2,4.3} @@ -71,6 +73,9 @@ deps = bottle-0.12: bottle>=0.12,<0.13 bottle-dev: git+https://github.com/bottlepy/bottle#egg=bottle + falcon-1.4: falcon>=1.4,<1.5 + falcon-dev: git+https://github.com/falconry/falcon#egg=falcon + sanic-0.8: sanic>=0.8,<0.9 sanic-18: sanic>=18.0,<19.0 sanic-19: sanic>=19.0,<20.0 @@ -129,6 +134,7 @@ setenv = django: TESTPATH=tests/integrations/django flask: TESTPATH=tests/integrations/flask bottle: TESTPATH=tests/integrations/bottle + falcon: TESTPATH=tests/integrations/falcon celery: TESTPATH=tests/integrations/celery requests: TESTPATH=tests/integrations/requests aws_lambda: TESTPATH=tests/integrations/aws_lambda @@ -149,6 +155,7 @@ usedevelop = True extras = flask: flask bottle: bottle + falcon: falcon basepython = py2.7: python2.7 From a86ec1c24060d9e4ad2845496c40d41ff717ba89 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Mon, 22 Apr 2019 11:08:54 +0200 Subject: [PATCH 02/10] `if False` typing import style + lint fixes --- sentry_sdk/integrations/falcon.py | 23 +++++++++-------------- tests/integrations/falcon/test_falcon.py | 18 ++++++++---------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index 105fbbcb2b..66dabc97d3 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -1,5 +1,3 @@ -from typing import Any, Callable, Dict - import falcon import falcon.api_helpers import sentry_sdk.integrations @@ -8,9 +6,13 @@ from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +if False: + from typing import Any + from typing import Callable + from typing import Dict -class FalconRequestExtractor(RequestExtractor): +class FalconRequestExtractor(RequestExtractor): def env(self): return self.request.env @@ -45,9 +47,7 @@ def process_request(self, req, resp, *args, **kwargs): return with hub.configure_scope() as scope: - scope.add_event_processor( - _make_request_event_processor(req, integration) - ) + scope.add_event_processor(_make_request_event_processor(req, integration)) class FalconIntegration(sentry_sdk.integrations.Integration): @@ -128,20 +128,15 @@ def _patch_prepare_middleware(): original_prepare_middleware = falcon.api_helpers.prepare_middleware def sentry_patched_prepare_middleware( - middleware=None, - independent_middleware=False + middleware=None, independent_middleware=False ): hub = Hub.current integration = hub.get_integration(FalconIntegration) if integration is not None: middleware = [SentryFalconMiddleware()] + (middleware or []) - return original_prepare_middleware( - middleware, - independent_middleware, - ) + return original_prepare_middleware(middleware, independent_middleware) - falcon.api_helpers.prepare_middleware = \ - sentry_patched_prepare_middleware + falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware def _make_request_event_processor(req, integration): diff --git a/tests/integrations/falcon/test_falcon.py b/tests/integrations/falcon/test_falcon.py index 3a22138183..20b8ab509a 100644 --- a/tests/integrations/falcon/test_falcon.py +++ b/tests/integrations/falcon/test_falcon.py @@ -16,13 +16,14 @@ def make_app(sentry_init): def inner(): class MessageResource: def on_get(self, req, resp): - sentry_sdk.capture_message('hi') - resp.media = 'hi' + sentry_sdk.capture_message("hi") + resp.media = "hi" app = falcon.API() - app.add_route('/message', MessageResource()) + app.add_route("/message", MessageResource()) return app + return inner @@ -31,6 +32,7 @@ def make_client(make_app): def inner(): app = make_app() return falcon.testing.TestClient(app) + return inner @@ -50,14 +52,12 @@ def test_has_context(sentry_init, capture_events, make_client): @pytest.mark.parametrize( "transaction_style,expected_transaction", - [("uri_template", "/message"), ("path", "/message")] + [("uri_template", "/message"), ("path", "/message")], ) def test_transaction_style( sentry_init, make_client, capture_events, transaction_style, expected_transaction ): - integration = falcon_sentry.FalconIntegration( - transaction_style=transaction_style - ) + integration = falcon_sentry.FalconIntegration(transaction_style=transaction_style) sentry_init(integrations=[integration]) events = capture_events() @@ -187,9 +187,7 @@ def on_get(self, req, resp): def http500_handler(ex, req, resp, params): sentry_sdk.capture_exception(ex) - resp.media = { - "message": "Sentry error: %s" % sentry_sdk.last_event_id() - } + resp.media = {"message": "Sentry error: %s" % sentry_sdk.last_event_id()} app.add_error_handler(Exception, http500_handler) From e4a45c761806971b690af8c6852a9bd9f16c0e38 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Mon, 22 Apr 2019 11:24:22 +0200 Subject: [PATCH 03/10] Remove redundant scope --- sentry_sdk/integrations/falcon.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index 66dabc97d3..686d41eb28 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -47,10 +47,11 @@ def process_request(self, req, resp, *args, **kwargs): return with hub.configure_scope() as scope: + scope._name = 'falcon' scope.add_event_processor(_make_request_event_processor(req, integration)) -class FalconIntegration(sentry_sdk.integrations.Integration): +class FalconIntegration(Integration): identifier = "falcon" transaction_style = None @@ -85,11 +86,8 @@ def sentry_patched_wsgi_app(self, env, start_response): sentry_wrapped = SentryWsgiMiddleware( lambda envi, start_resp: original_wsgi_app(self, envi, start_resp) ) - with hub.push_scope() as scope: - scope._name = "falcon" - resp = sentry_wrapped(env, start_response) - return resp + return sentry_wrapped(env, start_response) falcon.API.__call__ = sentry_patched_wsgi_app From 79a5e62b8dda3016bb192e16435f5948e6a3e595 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Mon, 22 Apr 2019 11:24:55 +0200 Subject: [PATCH 04/10] Fix absolute imports errors in pypy and py2.7 --- sentry_sdk/integrations/falcon.py | 4 +++- tests/integrations/falcon/test_falcon.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index 686d41eb28..a283b94ede 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -1,7 +1,9 @@ +from __future__ import absolute_import + import falcon import falcon.api_helpers -import sentry_sdk.integrations from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.utils import capture_internal_exceptions, event_from_exception diff --git a/tests/integrations/falcon/test_falcon.py b/tests/integrations/falcon/test_falcon.py index 20b8ab509a..96e8b05e61 100644 --- a/tests/integrations/falcon/test_falcon.py +++ b/tests/integrations/falcon/test_falcon.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import logging import pytest From c233eb3a1f884bee472b5c296c12b3a2c682daf3 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Mon, 22 Apr 2019 11:44:20 +0200 Subject: [PATCH 05/10] =?UTF-8?q?Try=20to=20return=20falcon.Request.media?= =?UTF-8?q?=20in=20case=20it=20wasn=E2=80=99t=20consumed=20in=20the=20requ?= =?UTF-8?q?est?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/falcon.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index a283b94ede..9e5db4ea6c 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -34,9 +34,14 @@ def files(self): return None # No such concept in Falcon def json(self): - # We don't touch falcon.Request.media as that can raise an exception - # on non-JSON requests. - return self.request._media + try: + return self.request.media + except falcon.errors.HTTPBadRequest: + # NOTE(jmagnusson): We return `falcon.Request._media` here because + # falcon 1.4 doesn't do proper type checking in + # `falcon.Request.media`. This has been fixed in 2.0. + # Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953 + return self.request._media class SentryFalconMiddleware(object): @@ -49,7 +54,7 @@ def process_request(self, req, resp, *args, **kwargs): return with hub.configure_scope() as scope: - scope._name = 'falcon' + scope._name = "falcon" scope.add_event_processor(_make_request_event_processor(req, integration)) From 5233e0c921d61feaa98cba2c435008f0f3b96791 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Mon, 22 Apr 2019 11:48:37 +0200 Subject: [PATCH 06/10] Send request data on non-JSON requests as well --- sentry_sdk/integrations/falcon.py | 17 +++++++++++------ tests/integrations/falcon/test_falcon.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index 9e5db4ea6c..4eb861c211 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -21,18 +21,23 @@ def env(self): def cookies(self): return self.request.cookies - def raw_data(self): - # As request data can only read once we won't make this available - # to Sentry. - # TODO(jmagnusson): Figure out if there's a way to support this - return None - def form(self): return None # No such concept in Falcon def files(self): return None # No such concept in Falcon + def raw_data(self): + # As request data can only be read once we won't make this available + # to Sentry. Just send back a dummy string in case there was a + # content length. + # TODO(jmagnusson): Figure out if there's a way to support this + content_length = self.content_length() + if content_length > 0: + return "[REQUEST_CONTAINING_RAW_DATA]" + else: + return None + def json(self): try: return self.request.media diff --git a/tests/integrations/falcon/test_falcon.py b/tests/integrations/falcon/test_falcon.py index 96e8b05e61..4fd18dcf2f 100644 --- a/tests/integrations/falcon/test_falcon.py +++ b/tests/integrations/falcon/test_falcon.py @@ -148,6 +148,28 @@ def on_post(self, req, resp): assert event["request"]["data"] == data +def test_falcon_raw_data_request(sentry_init, capture_events): + sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + + class Resource: + def on_post(self, req, resp): + sentry_sdk.capture_message("hi") + resp.media = "ok" + + app = falcon.API() + app.add_route("/", Resource()) + + events = capture_events() + + client = falcon.testing.TestClient(app) + response = client.simulate_post("/", body='hi') + assert response.status == falcon.HTTP_200 + + event, = events + assert event["request"]["headers"]["Content-Length"] == "2" + assert event["request"]["data"] == "" + + def test_logging(sentry_init, capture_events): sentry_init( integrations=[ From cdc623610cefd50ea94062932c949f05dc09f9d2 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Mon, 22 Apr 2019 12:08:09 +0200 Subject: [PATCH 07/10] Lint fix --- tests/integrations/falcon/test_falcon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/falcon/test_falcon.py b/tests/integrations/falcon/test_falcon.py index 4fd18dcf2f..c6081933e6 100644 --- a/tests/integrations/falcon/test_falcon.py +++ b/tests/integrations/falcon/test_falcon.py @@ -162,7 +162,7 @@ def on_post(self, req, resp): events = capture_events() client = falcon.testing.TestClient(app) - response = client.simulate_post("/", body='hi') + response = client.simulate_post("/", body="hi") assert response.status == falcon.HTTP_200 event, = events From e120d90b31befed9fadd8bfb07a837aae0e995ea Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Mon, 22 Apr 2019 21:11:12 +0200 Subject: [PATCH 08/10] Fix mypy failing --- sentry_sdk/integrations/falcon.py | 4 ++-- tests/integrations/falcon/test_falcon.py | 26 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index 4eb861c211..317348298b 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -1,7 +1,7 @@ from __future__ import absolute_import -import falcon -import falcon.api_helpers +import falcon # type: ignore +import falcon.api_helpers # type: ignore from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi_common import RequestExtractor diff --git a/tests/integrations/falcon/test_falcon.py b/tests/integrations/falcon/test_falcon.py index c6081933e6..5ad8a0e690 100644 --- a/tests/integrations/falcon/test_falcon.py +++ b/tests/integrations/falcon/test_falcon.py @@ -9,7 +9,7 @@ import falcon import falcon.testing import sentry_sdk -import sentry_sdk.integrations.falcon as falcon_sentry +from sentry_sdk.integrations.falcon import FalconIntegration from sentry_sdk.integrations.logging import LoggingIntegration @@ -39,7 +39,7 @@ def inner(): def test_has_context(sentry_init, capture_events, make_client): - sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + sentry_init(integrations=[FalconIntegration()]) events = capture_events() client = make_client() @@ -59,7 +59,7 @@ def test_has_context(sentry_init, capture_events, make_client): def test_transaction_style( sentry_init, make_client, capture_events, transaction_style, expected_transaction ): - integration = falcon_sentry.FalconIntegration(transaction_style=transaction_style) + integration = FalconIntegration(transaction_style=transaction_style) sentry_init(integrations=[integration]) events = capture_events() @@ -72,7 +72,7 @@ def test_transaction_style( def test_errors(sentry_init, capture_exceptions, capture_events): - sentry_init(integrations=[falcon_sentry.FalconIntegration()], debug=True) + sentry_init(integrations=[FalconIntegration()], debug=True) class ZeroDivisionErrorResource: def on_get(self, req, resp): @@ -99,7 +99,7 @@ def on_get(self, req, resp): def test_falcon_large_json_request(sentry_init, capture_events): - sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + sentry_init(integrations=[FalconIntegration()]) data = {"foo": {"bar": "a" * 2000}} @@ -127,7 +127,7 @@ def on_post(self, req, resp): @pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"]) def test_falcon_empty_json_request(sentry_init, capture_events, data): - sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + sentry_init(integrations=[FalconIntegration()]) class Resource: def on_post(self, req, resp): @@ -149,7 +149,7 @@ def on_post(self, req, resp): def test_falcon_raw_data_request(sentry_init, capture_events): - sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + sentry_init(integrations=[FalconIntegration()]) class Resource: def on_post(self, req, resp): @@ -173,7 +173,7 @@ def on_post(self, req, resp): def test_logging(sentry_init, capture_events): sentry_init( integrations=[ - falcon_sentry.FalconIntegration(), + FalconIntegration(), LoggingIntegration(event_level="ERROR"), ] ) @@ -199,7 +199,7 @@ def on_get(self, req, resp): def test_500(sentry_init, capture_events): - sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + sentry_init(integrations=[FalconIntegration()]) app = falcon.API() @@ -225,7 +225,7 @@ def http500_handler(ex, req, resp, params): def test_error_in_errorhandler(sentry_init, capture_events): - sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + sentry_init(integrations=[FalconIntegration()]) app = falcon.API() @@ -258,7 +258,7 @@ def http500_handler(ex, req, resp, params): def test_bad_request_not_captured(sentry_init, capture_events): - sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + sentry_init(integrations=[FalconIntegration()]) events = capture_events() app = falcon.API() @@ -277,7 +277,7 @@ def on_get(self, req, resp): def test_does_not_leak_scope(sentry_init, capture_events): - sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + sentry_init(integrations=[FalconIntegration()]) events = capture_events() with sentry_sdk.configure_scope() as scope: @@ -316,7 +316,7 @@ def generator(): def test_errorhandler_for_exception_swallows_exception( sentry_init, capture_events, exc_cls ): - sentry_init(integrations=[falcon_sentry.FalconIntegration()]) + sentry_init(integrations=[FalconIntegration()]) events = capture_events() app = falcon.API() From f2404d496ee4ef6b1500cd70b1ed3386d23c358d Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Mon, 22 Apr 2019 21:36:58 +0200 Subject: [PATCH 09/10] Automatically capture internal server errors when an error handler has been defined --- sentry_sdk/integrations/falcon.py | 6 +++- tests/integrations/falcon/test_falcon.py | 43 +++--------------------- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index 317348298b..da641088ba 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -121,7 +121,7 @@ def sentry_patched_handle_exception(self, *args): hub = Hub.current integration = hub.get_integration(FalconIntegration) - if integration is not None and not was_handled: + if integration is not None and not _is_falcon_http_error(ex): event, hint = event_from_exception( ex, client_options=hub.client.options, @@ -149,6 +149,10 @@ def sentry_patched_prepare_middleware( falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware +def _is_falcon_http_error(ex): + return isinstance(ex, (falcon.HTTPError, falcon.http_status.HTTPStatus)) + + def _make_request_event_processor(req, integration): # type: (falcon.Request, FalconIntegration) -> Callable diff --git a/tests/integrations/falcon/test_falcon.py b/tests/integrations/falcon/test_falcon.py index 5ad8a0e690..995cb26a67 100644 --- a/tests/integrations/falcon/test_falcon.py +++ b/tests/integrations/falcon/test_falcon.py @@ -172,10 +172,7 @@ def on_post(self, req, resp): def test_logging(sentry_init, capture_events): sentry_init( - integrations=[ - FalconIntegration(), - LoggingIntegration(event_level="ERROR"), - ] + integrations=[FalconIntegration(), LoggingIntegration(event_level="ERROR")] ) logger = logging.getLogger() @@ -236,7 +233,6 @@ def on_get(self, req, resp): app.add_route("/", Resource()) def http500_handler(ex, req, resp, params): - sentry_sdk.capture_exception(ex) 1 / 0 app.add_error_handler(Exception, http500_handler) @@ -248,13 +244,11 @@ def http500_handler(ex, req, resp, params): with pytest.raises(ZeroDivisionError): client.simulate_get("/") - event1, event2 = events - - exception, = event1["exception"]["values"] - assert exception["type"] == "ValueError" + event, = events - exception = event2["exception"]["values"][-1] - assert exception["type"] == "ZeroDivisionError" + last_ex_values = event["exception"]["values"][-1] + assert last_ex_values["type"] == "ZeroDivisionError" + assert last_ex_values["stacktrace"]["frames"][-1]["vars"]["ex"] == "ValueError()" def test_bad_request_not_captured(sentry_init, capture_events): @@ -310,30 +304,3 @@ def generator(): with sentry_sdk.configure_scope() as scope: assert not scope._tags["request_data"] - - -@pytest.mark.parametrize("exc_cls", [ZeroDivisionError, Exception]) -def test_errorhandler_for_exception_swallows_exception( - sentry_init, capture_events, exc_cls -): - sentry_init(integrations=[FalconIntegration()]) - events = capture_events() - - app = falcon.API() - - class Resource: - def on_get(self, req, resp): - 1 / 0 - - app.add_route("/", Resource()) - - def exc_handler(ex, req, resp, params): - resp.media = "ok" - - app.add_error_handler(exc_cls, exc_handler) - - client = falcon.testing.TestClient(app) - - response = client.simulate_get("/") - assert response.status == falcon.HTTP_200 - assert not events From 813f8f50ebe58faca7f4a5f7bf1bab325db66a11 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Tue, 23 Apr 2019 11:14:41 +0200 Subject: [PATCH 10/10] Add falcon 2.0 to test suite --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 76b72ab5ee..0dc3795a17 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ envlist = {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-bottle-0.12 {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-falcon-1.4 + {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-falcon-2.0 {py3.5,py3.6,py3.7}-sanic-{0.8,18,19} @@ -74,7 +75,7 @@ deps = bottle-dev: git+https://github.com/bottlepy/bottle#egg=bottle falcon-1.4: falcon>=1.4,<1.5 - falcon-dev: git+https://github.com/falconry/falcon#egg=falcon + falcon-2.0: falcon>=2.0.0rc3,<3.0 sanic-0.8: sanic>=0.8,<0.9 sanic-18: sanic>=18.0,<19.0