Skip to content

Commit f62c83d

Browse files
feat(falcon): Update of Falcon Integration (#1733)
Update Falcon Integration to support Falcon 3.x --------- Co-authored-by: bartolootrit <[email protected]>
1 parent f21fc0f commit f62c83d

File tree

8 files changed

+141
-85
lines changed

8 files changed

+141
-85
lines changed

.github/workflows/test-integration-falcon.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
strategy:
3232
fail-fast: false
3333
matrix:
34-
python-version: ["2.7","3.5","3.6","3.7","3.8","3.9","3.10","3.11"]
34+
python-version: ["2.7","3.5","3.6","3.7","3.8","3.9"]
3535
# python3.6 reached EOL and is no longer being supported on
3636
# new versions of hosted runners on Github Actions
3737
# ubuntu-20.04 is the last version that supported python3.6

sentry_sdk/integrations/falcon.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,29 @@
1919

2020
from sentry_sdk._types import EventProcessor
2121

22+
# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers`
23+
# and `falcon.API` to `falcon.App`
24+
2225
try:
2326
import falcon # type: ignore
24-
import falcon.api_helpers # type: ignore
2527

2628
from falcon import __version__ as FALCON_VERSION
2729
except ImportError:
2830
raise DidNotEnable("Falcon not installed")
2931

32+
try:
33+
import falcon.app_helpers # type: ignore
34+
35+
falcon_helpers = falcon.app_helpers
36+
falcon_app_class = falcon.App
37+
FALCON3 = True
38+
except ImportError:
39+
import falcon.api_helpers # type: ignore
40+
41+
falcon_helpers = falcon.api_helpers
42+
falcon_app_class = falcon.API
43+
FALCON3 = False
44+
3045

3146
class FalconRequestExtractor(RequestExtractor):
3247
def env(self):
@@ -58,16 +73,27 @@ def raw_data(self):
5873
else:
5974
return None
6075

61-
def json(self):
62-
# type: () -> Optional[Dict[str, Any]]
63-
try:
64-
return self.request.media
65-
except falcon.errors.HTTPBadRequest:
66-
# NOTE(jmagnusson): We return `falcon.Request._media` here because
67-
# falcon 1.4 doesn't do proper type checking in
68-
# `falcon.Request.media`. This has been fixed in 2.0.
69-
# Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
70-
return self.request._media
76+
if FALCON3:
77+
78+
def json(self):
79+
# type: () -> Optional[Dict[str, Any]]
80+
try:
81+
return self.request.media
82+
except falcon.errors.HTTPBadRequest:
83+
return None
84+
85+
else:
86+
87+
def json(self):
88+
# type: () -> Optional[Dict[str, Any]]
89+
try:
90+
return self.request.media
91+
except falcon.errors.HTTPBadRequest:
92+
# NOTE(jmagnusson): We return `falcon.Request._media` here because
93+
# falcon 1.4 doesn't do proper type checking in
94+
# `falcon.Request.media`. This has been fixed in 2.0.
95+
# Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
96+
return self.request._media
7197

7298

7399
class SentryFalconMiddleware(object):
@@ -120,7 +146,7 @@ def setup_once():
120146

121147
def _patch_wsgi_app():
122148
# type: () -> None
123-
original_wsgi_app = falcon.API.__call__
149+
original_wsgi_app = falcon_app_class.__call__
124150

125151
def sentry_patched_wsgi_app(self, env, start_response):
126152
# type: (falcon.API, Any, Any) -> Any
@@ -135,12 +161,12 @@ def sentry_patched_wsgi_app(self, env, start_response):
135161

136162
return sentry_wrapped(env, start_response)
137163

138-
falcon.API.__call__ = sentry_patched_wsgi_app
164+
falcon_app_class.__call__ = sentry_patched_wsgi_app
139165

140166

141167
def _patch_handle_exception():
142168
# type: () -> None
143-
original_handle_exception = falcon.API._handle_exception
169+
original_handle_exception = falcon_app_class._handle_exception
144170

145171
def sentry_patched_handle_exception(self, *args):
146172
# type: (falcon.API, *Any) -> Any
@@ -170,12 +196,12 @@ def sentry_patched_handle_exception(self, *args):
170196

171197
return was_handled
172198

173-
falcon.API._handle_exception = sentry_patched_handle_exception
199+
falcon_app_class._handle_exception = sentry_patched_handle_exception
174200

175201

176202
def _patch_prepare_middleware():
177203
# type: () -> None
178-
original_prepare_middleware = falcon.api_helpers.prepare_middleware
204+
original_prepare_middleware = falcon_helpers.prepare_middleware
179205

180206
def sentry_patched_prepare_middleware(
181207
middleware=None, independent_middleware=False
@@ -187,7 +213,7 @@ def sentry_patched_prepare_middleware(
187213
middleware = [SentryFalconMiddleware()] + (middleware or [])
188214
return original_prepare_middleware(middleware, independent_middleware)
189215

190-
falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware
216+
falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware
191217

192218

193219
def _exception_leads_to_http_5xx(ex):

test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ jsonschema==3.2.0
1111
pyrsistent==0.16.0 # TODO(py3): 0.17.0 requires python3, see https://github.com/tobgu/pyrsistent/issues/205
1212
executing
1313
asttokens
14+
responses
1415
ipdb
Lines changed: 68 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,83 @@
11
import asyncio
22

3+
import pytest
34
import httpx
5+
import responses
46

57
from sentry_sdk import capture_message, start_transaction
68
from sentry_sdk.integrations.httpx import HttpxIntegration
79

810

9-
def test_crumb_capture_and_hint(sentry_init, capture_events):
11+
@pytest.mark.parametrize(
12+
"httpx_client",
13+
(httpx.Client(), httpx.AsyncClient()),
14+
)
15+
def test_crumb_capture_and_hint(sentry_init, capture_events, httpx_client):
1016
def before_breadcrumb(crumb, hint):
1117
crumb["data"]["extra"] = "foo"
1218
return crumb
1319

1420
sentry_init(integrations=[HttpxIntegration()], before_breadcrumb=before_breadcrumb)
15-
clients = (httpx.Client(), httpx.AsyncClient())
16-
for i, c in enumerate(clients):
17-
with start_transaction():
18-
events = capture_events()
19-
20-
url = "https://httpbin.org/status/200"
21-
if not asyncio.iscoroutinefunction(c.get):
22-
response = c.get(url)
23-
else:
24-
response = asyncio.get_event_loop().run_until_complete(c.get(url))
25-
26-
assert response.status_code == 200
27-
capture_message("Testing!")
28-
29-
(event,) = events
30-
# send request twice so we need get breadcrumb by index
31-
crumb = event["breadcrumbs"]["values"][i]
32-
assert crumb["type"] == "http"
33-
assert crumb["category"] == "httplib"
34-
assert crumb["data"] == {
35-
"url": url,
36-
"method": "GET",
37-
"http.fragment": "",
38-
"http.query": "",
39-
"status_code": 200,
40-
"reason": "OK",
41-
"extra": "foo",
42-
}
43-
44-
45-
def test_outgoing_trace_headers(sentry_init):
21+
22+
url = "http://example.com/"
23+
responses.add(responses.GET, url, status=200)
24+
25+
with start_transaction():
26+
events = capture_events()
27+
28+
if asyncio.iscoroutinefunction(httpx_client.get):
29+
response = asyncio.get_event_loop().run_until_complete(
30+
httpx_client.get(url)
31+
)
32+
else:
33+
response = httpx_client.get(url)
34+
35+
assert response.status_code == 200
36+
capture_message("Testing!")
37+
38+
(event,) = events
39+
40+
crumb = event["breadcrumbs"]["values"][0]
41+
assert crumb["type"] == "http"
42+
assert crumb["category"] == "httplib"
43+
assert crumb["data"] == {
44+
"url": url,
45+
"method": "GET",
46+
"http.fragment": "",
47+
"http.query": "",
48+
"status_code": 200,
49+
"reason": "OK",
50+
"extra": "foo",
51+
}
52+
53+
54+
@pytest.mark.parametrize(
55+
"httpx_client",
56+
(httpx.Client(), httpx.AsyncClient()),
57+
)
58+
def test_outgoing_trace_headers(sentry_init, httpx_client):
4659
sentry_init(traces_sample_rate=1.0, integrations=[HttpxIntegration()])
47-
clients = (httpx.Client(), httpx.AsyncClient())
48-
for i, c in enumerate(clients):
49-
with start_transaction(
50-
name="/interactions/other-dogs/new-dog",
51-
op="greeting.sniff",
52-
# make trace_id difference between transactions
53-
trace_id=f"012345678901234567890123456789{i}",
54-
) as transaction:
55-
url = "https://httpbin.org/status/200"
56-
if not asyncio.iscoroutinefunction(c.get):
57-
response = c.get(url)
58-
else:
59-
response = asyncio.get_event_loop().run_until_complete(c.get(url))
60-
61-
request_span = transaction._span_recorder.spans[-1]
62-
assert response.request.headers[
63-
"sentry-trace"
64-
] == "{trace_id}-{parent_span_id}-{sampled}".format(
65-
trace_id=transaction.trace_id,
66-
parent_span_id=request_span.span_id,
67-
sampled=1,
60+
61+
url = "http://example.com/"
62+
responses.add(responses.GET, url, status=200)
63+
64+
with start_transaction(
65+
name="/interactions/other-dogs/new-dog",
66+
op="greeting.sniff",
67+
trace_id="01234567890123456789012345678901",
68+
) as transaction:
69+
if asyncio.iscoroutinefunction(httpx_client.get):
70+
response = asyncio.get_event_loop().run_until_complete(
71+
httpx_client.get(url)
6872
)
73+
else:
74+
response = httpx_client.get(url)
75+
76+
request_span = transaction._span_recorder.spans[-1]
77+
assert response.request.headers[
78+
"sentry-trace"
79+
] == "{trace_id}-{parent_span_id}-{sampled}".format(
80+
trace_id=transaction.trace_id,
81+
parent_span_id=request_span.span_id,
82+
sampled=1,
83+
)

tests/integrations/opentelemetry/test_span_processor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,14 +212,14 @@ def test_update_span_with_otel_data_http_method2():
212212
"http.status_code": 429,
213213
"http.status_text": "xxx",
214214
"http.user_agent": "curl/7.64.1",
215-
"http.url": "https://httpbin.org/status/403?password=123&[email protected]&author=User123&auth=1234567890abcdef",
215+
"http.url": "https://example.com/status/403?password=123&[email protected]&author=User123&auth=1234567890abcdef",
216216
}
217217

218218
span_processor = SentrySpanProcessor()
219219
span_processor._update_span_with_otel_data(sentry_span, otel_span)
220220

221221
assert sentry_span.op == "http.server"
222-
assert sentry_span.description == "GET https://httpbin.org/status/403"
222+
assert sentry_span.description == "GET https://example.com/status/403"
223223
assert sentry_span._tags["http.status_code"] == "429"
224224
assert sentry_span.status == "resource_exhausted"
225225

@@ -229,7 +229,7 @@ def test_update_span_with_otel_data_http_method2():
229229
assert sentry_span._data["http.user_agent"] == "curl/7.64.1"
230230
assert (
231231
sentry_span._data["http.url"]
232-
== "https://httpbin.org/status/403?password=123&[email protected]&author=User123&auth=1234567890abcdef"
232+
== "https://example.com/status/403?password=123&[email protected]&author=User123&auth=1234567890abcdef"
233233
)
234234

235235

tests/integrations/requests/test_requests.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
import responses
23

34
requests = pytest.importorskip("requests")
45

@@ -8,17 +9,21 @@
89

910
def test_crumb_capture(sentry_init, capture_events):
1011
sentry_init(integrations=[StdlibIntegration()])
12+
13+
url = "http://example.com/"
14+
responses.add(responses.GET, url, status=200)
15+
1116
events = capture_events()
1217

13-
response = requests.get("https://httpbin.org/status/418")
18+
response = requests.get(url)
1419
capture_message("Testing!")
1520

1621
(event,) = events
1722
(crumb,) = event["breadcrumbs"]["values"]
1823
assert crumb["type"] == "http"
1924
assert crumb["category"] == "httplib"
2025
assert crumb["data"] == {
21-
"url": "https://httpbin.org/status/418",
26+
"url": url,
2227
"method": "GET",
2328
"http.fragment": "",
2429
"http.query": "",

0 commit comments

Comments
 (0)