diff --git a/.github/workflows/test-integration-falcon.yml b/.github/workflows/test-integration-falcon.yml index f69ac1d9cd..259006f106 100644 --- a/.github/workflows/test-integration-falcon.yml +++ b/.github/workflows/test-integration-falcon.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["2.7","3.5","3.6","3.7","3.8","3.9","3.10","3.11"] + python-version: ["2.7","3.5","3.6","3.7","3.8","3.9"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index b38e4bd5b4..fd4648a4b6 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -19,14 +19,29 @@ from sentry_sdk._types import EventProcessor +# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers` +# and `falcon.API` to `falcon.App` + try: import falcon # type: ignore - import falcon.api_helpers # type: ignore from falcon import __version__ as FALCON_VERSION except ImportError: raise DidNotEnable("Falcon not installed") +try: + import falcon.app_helpers # type: ignore + + falcon_helpers = falcon.app_helpers + falcon_app_class = falcon.App + FALCON3 = True +except ImportError: + import falcon.api_helpers # type: ignore + + falcon_helpers = falcon.api_helpers + falcon_app_class = falcon.API + FALCON3 = False + class FalconRequestExtractor(RequestExtractor): def env(self): @@ -58,16 +73,27 @@ def raw_data(self): else: return None - def json(self): - # type: () -> Optional[Dict[str, Any]] - 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 + if FALCON3: + + def json(self): + # type: () -> Optional[Dict[str, Any]] + try: + return self.request.media + except falcon.errors.HTTPBadRequest: + return None + + else: + + def json(self): + # type: () -> Optional[Dict[str, Any]] + 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): @@ -120,7 +146,7 @@ def setup_once(): def _patch_wsgi_app(): # type: () -> None - original_wsgi_app = falcon.API.__call__ + original_wsgi_app = falcon_app_class.__call__ def sentry_patched_wsgi_app(self, env, start_response): # type: (falcon.API, Any, Any) -> Any @@ -135,12 +161,12 @@ def sentry_patched_wsgi_app(self, env, start_response): return sentry_wrapped(env, start_response) - falcon.API.__call__ = sentry_patched_wsgi_app + falcon_app_class.__call__ = sentry_patched_wsgi_app def _patch_handle_exception(): # type: () -> None - original_handle_exception = falcon.API._handle_exception + original_handle_exception = falcon_app_class._handle_exception def sentry_patched_handle_exception(self, *args): # type: (falcon.API, *Any) -> Any @@ -170,12 +196,12 @@ def sentry_patched_handle_exception(self, *args): return was_handled - falcon.API._handle_exception = sentry_patched_handle_exception + falcon_app_class._handle_exception = sentry_patched_handle_exception def _patch_prepare_middleware(): # type: () -> None - original_prepare_middleware = falcon.api_helpers.prepare_middleware + original_prepare_middleware = falcon_helpers.prepare_middleware def sentry_patched_prepare_middleware( middleware=None, independent_middleware=False @@ -187,7 +213,7 @@ def sentry_patched_prepare_middleware( middleware = [SentryFalconMiddleware()] + (middleware or []) return original_prepare_middleware(middleware, independent_middleware) - falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware + falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware def _exception_leads_to_http_5xx(ex): diff --git a/test-requirements.txt b/test-requirements.txt index 4c40e801bf..5d449df716 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,4 +11,5 @@ jsonschema==3.2.0 pyrsistent==0.16.0 # TODO(py3): 0.17.0 requires python3, see https://github.com/tobgu/pyrsistent/issues/205 executing asttokens +responses ipdb diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index 0597d10988..9945440c3a 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -1,68 +1,83 @@ import asyncio +import pytest import httpx +import responses from sentry_sdk import capture_message, start_transaction from sentry_sdk.integrations.httpx import HttpxIntegration -def test_crumb_capture_and_hint(sentry_init, capture_events): +@pytest.mark.parametrize( + "httpx_client", + (httpx.Client(), httpx.AsyncClient()), +) +def test_crumb_capture_and_hint(sentry_init, capture_events, httpx_client): def before_breadcrumb(crumb, hint): crumb["data"]["extra"] = "foo" return crumb sentry_init(integrations=[HttpxIntegration()], before_breadcrumb=before_breadcrumb) - clients = (httpx.Client(), httpx.AsyncClient()) - for i, c in enumerate(clients): - with start_transaction(): - events = capture_events() - - url = "https://httpbin.org/status/200" - if not asyncio.iscoroutinefunction(c.get): - response = c.get(url) - else: - response = asyncio.get_event_loop().run_until_complete(c.get(url)) - - assert response.status_code == 200 - capture_message("Testing!") - - (event,) = events - # send request twice so we need get breadcrumb by index - crumb = event["breadcrumbs"]["values"][i] - assert crumb["type"] == "http" - assert crumb["category"] == "httplib" - assert crumb["data"] == { - "url": url, - "method": "GET", - "http.fragment": "", - "http.query": "", - "status_code": 200, - "reason": "OK", - "extra": "foo", - } - - -def test_outgoing_trace_headers(sentry_init): + + url = "http://example.com/" + responses.add(responses.GET, url, status=200) + + with start_transaction(): + events = capture_events() + + if asyncio.iscoroutinefunction(httpx_client.get): + response = asyncio.get_event_loop().run_until_complete( + httpx_client.get(url) + ) + else: + response = httpx_client.get(url) + + assert response.status_code == 200 + capture_message("Testing!") + + (event,) = events + + crumb = event["breadcrumbs"]["values"][0] + assert crumb["type"] == "http" + assert crumb["category"] == "httplib" + assert crumb["data"] == { + "url": url, + "method": "GET", + "http.fragment": "", + "http.query": "", + "status_code": 200, + "reason": "OK", + "extra": "foo", + } + + +@pytest.mark.parametrize( + "httpx_client", + (httpx.Client(), httpx.AsyncClient()), +) +def test_outgoing_trace_headers(sentry_init, httpx_client): sentry_init(traces_sample_rate=1.0, integrations=[HttpxIntegration()]) - clients = (httpx.Client(), httpx.AsyncClient()) - for i, c in enumerate(clients): - with start_transaction( - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - # make trace_id difference between transactions - trace_id=f"012345678901234567890123456789{i}", - ) as transaction: - url = "https://httpbin.org/status/200" - if not asyncio.iscoroutinefunction(c.get): - response = c.get(url) - else: - response = asyncio.get_event_loop().run_until_complete(c.get(url)) - - request_span = transaction._span_recorder.spans[-1] - assert response.request.headers[ - "sentry-trace" - ] == "{trace_id}-{parent_span_id}-{sampled}".format( - trace_id=transaction.trace_id, - parent_span_id=request_span.span_id, - sampled=1, + + url = "http://example.com/" + responses.add(responses.GET, url, status=200) + + with start_transaction( + name="/interactions/other-dogs/new-dog", + op="greeting.sniff", + trace_id="01234567890123456789012345678901", + ) as transaction: + if asyncio.iscoroutinefunction(httpx_client.get): + response = asyncio.get_event_loop().run_until_complete( + httpx_client.get(url) ) + else: + response = httpx_client.get(url) + + request_span = transaction._span_recorder.spans[-1] + assert response.request.headers[ + "sentry-trace" + ] == "{trace_id}-{parent_span_id}-{sampled}".format( + trace_id=transaction.trace_id, + parent_span_id=request_span.span_id, + sampled=1, + ) diff --git a/tests/integrations/opentelemetry/test_span_processor.py b/tests/integrations/opentelemetry/test_span_processor.py index d7dc6b66df..0467da7673 100644 --- a/tests/integrations/opentelemetry/test_span_processor.py +++ b/tests/integrations/opentelemetry/test_span_processor.py @@ -212,14 +212,14 @@ def test_update_span_with_otel_data_http_method2(): "http.status_code": 429, "http.status_text": "xxx", "http.user_agent": "curl/7.64.1", - "http.url": "https://httpbin.org/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef", + "http.url": "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef", } span_processor = SentrySpanProcessor() span_processor._update_span_with_otel_data(sentry_span, otel_span) assert sentry_span.op == "http.server" - assert sentry_span.description == "GET https://httpbin.org/status/403" + assert sentry_span.description == "GET https://example.com/status/403" assert sentry_span._tags["http.status_code"] == "429" assert sentry_span.status == "resource_exhausted" @@ -229,7 +229,7 @@ def test_update_span_with_otel_data_http_method2(): assert sentry_span._data["http.user_agent"] == "curl/7.64.1" assert ( sentry_span._data["http.url"] - == "https://httpbin.org/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef" + == "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef" ) diff --git a/tests/integrations/requests/test_requests.py b/tests/integrations/requests/test_requests.py index f4c6b01db0..7070895dfc 100644 --- a/tests/integrations/requests/test_requests.py +++ b/tests/integrations/requests/test_requests.py @@ -1,4 +1,5 @@ import pytest +import responses requests = pytest.importorskip("requests") @@ -8,9 +9,13 @@ def test_crumb_capture(sentry_init, capture_events): sentry_init(integrations=[StdlibIntegration()]) + + url = "http://example.com/" + responses.add(responses.GET, url, status=200) + events = capture_events() - response = requests.get("https://httpbin.org/status/418") + response = requests.get(url) capture_message("Testing!") (event,) = events @@ -18,7 +23,7 @@ def test_crumb_capture(sentry_init, capture_events): assert crumb["type"] == "http" assert crumb["category"] == "httplib" assert crumb["data"] == { - "url": "https://httpbin.org/status/418", + "url": url, "method": "GET", "http.fragment": "", "http.query": "", diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 3943506fbf..a66a20c431 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -1,6 +1,7 @@ import platform import sys import random +import responses import pytest try: @@ -29,9 +30,12 @@ def test_crumb_capture(sentry_init, capture_events): sentry_init(integrations=[StdlibIntegration()]) + + url = "http://example.com/" + responses.add(responses.GET, url, status=200) + events = capture_events() - url = "https://httpbin.org/status/200" response = urlopen(url) assert response.getcode() == 200 capture_message("Testing!") @@ -56,9 +60,12 @@ def before_breadcrumb(crumb, hint): return crumb sentry_init(integrations=[StdlibIntegration()], before_breadcrumb=before_breadcrumb) + + url = "http://example.com/" + responses.add(responses.GET, url, status=200) + events = capture_events() - url = "https://httpbin.org/status/200" response = urlopen(url) assert response.getcode() == 200 capture_message("Testing!") @@ -88,7 +95,7 @@ def test_empty_realurl(sentry_init, capture_events): """ sentry_init(dsn="") - HTTPConnection("httpbin.org", port=443).putrequest("POST", None) + HTTPConnection("example.com", port=443).putrequest("POST", None) def test_httplib_misuse(sentry_init, capture_events, request): @@ -104,19 +111,19 @@ def test_httplib_misuse(sentry_init, capture_events, request): sentry_init() events = capture_events() - conn = HTTPSConnection("httpbin.org", 443) + conn = HTTPSConnection("httpstat.us", 443) # make sure we release the resource, even if the test fails request.addfinalizer(conn.close) - conn.request("GET", "/anything/foo") + conn.request("GET", "/200") with pytest.raises(Exception): # This raises an exception, because we didn't call `getresponse` for # the previous request yet. # # This call should not affect our breadcrumb. - conn.request("POST", "/anything/bar") + conn.request("POST", "/200") response = conn.getresponse() assert response._method == "GET" @@ -129,7 +136,7 @@ def test_httplib_misuse(sentry_init, capture_events, request): assert crumb["type"] == "http" assert crumb["category"] == "httplib" assert crumb["data"] == { - "url": "https://httpbin.org/anything/foo", + "url": "https://httpstat.us/200", "method": "GET", "status_code": 200, "reason": "OK", diff --git a/tox.ini b/tox.ini index cda2e6ccf6..d1b058dc71 100644 --- a/tox.ini +++ b/tox.ini @@ -64,8 +64,9 @@ envlist = # Falcon {py2.7,py3.5,py3.6,py3.7}-falcon-v{1.4} - {py2.7,py3.5,py3.6,py3.7,py3.8,py3.9,py3.10,py3.11}-falcon-v{2.0} - + {py2.7,py3.5,py3.6,py3.7}-falcon-v{2.0} + {py3.5,py3.6,py3.7,py3.8,py3.9}-falcon-v{3.0} + # FastAPI {py3.7,py3.8,py3.9,py3.10,py3.11}-fastapi @@ -245,6 +246,7 @@ deps = # Falcon falcon-v1.4: falcon>=1.4,<1.5 falcon-v2.0: falcon>=2.0.0rc3,<3.0 + falcon-v3.0: falcon>=3.0.0,<3.1.0 # FastAPI fastapi: fastapi