Skip to content

Commit 82289f5

Browse files
authored
feat: Pyramid integration (#135)
* feat: Pyramid integration * build: Add pyramid build matrix * fix: Skip tests if pyramid not installed
1 parent 038e8ed commit 82289f5

File tree

6 files changed

+409
-3
lines changed

6 files changed

+409
-3
lines changed

sentry_sdk/integrations/_wsgi.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
capture_internal_exceptions,
88
event_from_exception,
99
)
10-
from sentry_sdk._compat import reraise, implements_iterator
10+
from sentry_sdk._compat import reraise, implements_iterator, text_type
1111

1212

1313
def get_environ(environ):
@@ -123,7 +123,10 @@ def is_json(self):
123123
def json(self):
124124
try:
125125
if self.is_json():
126-
return json.loads(self.raw_data().decode("utf-8"))
126+
raw_data = self.raw_data()
127+
if not isinstance(raw_data, text_type):
128+
raw_data = raw_data.decode("utf-8")
129+
return json.loads(raw_data)
127130
except ValueError:
128131
pass
129132

sentry_sdk/integrations/pyramid.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from __future__ import absolute_import
2+
3+
import os
4+
import sys
5+
import weakref
6+
7+
from pyramid.httpexceptions import HTTPException
8+
9+
from sentry_sdk.hub import Hub, _should_send_default_pii
10+
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
11+
from sentry_sdk._compat import reraise
12+
13+
from sentry_sdk.integrations import Integration
14+
from sentry_sdk.integrations._wsgi import RequestExtractor, run_wsgi_app
15+
16+
17+
class PyramidIntegration(Integration):
18+
identifier = "pyramid"
19+
20+
transaction_style = None
21+
22+
def __init__(self, transaction_style="route_name"):
23+
TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern")
24+
if transaction_style not in TRANSACTION_STYLE_VALUES:
25+
raise ValueError(
26+
"Invalid value for transaction_style: %s (must be in %s)"
27+
% (transaction_style, TRANSACTION_STYLE_VALUES)
28+
)
29+
self.transaction_style = transaction_style
30+
31+
@staticmethod
32+
def setup_once():
33+
from pyramid.router import Router
34+
35+
old_handle_request = Router.handle_request
36+
37+
def sentry_patched_handle_request(self, request, *args, **kwargs):
38+
hub = Hub.current
39+
integration = hub.get_integration(PyramidIntegration)
40+
if integration is None:
41+
return old_handle_request(self, request, *args, **kwargs)
42+
43+
with hub.configure_scope() as scope:
44+
scope.add_event_processor(
45+
_make_event_processor(weakref.ref(request), integration)
46+
)
47+
48+
try:
49+
return old_handle_request(self, request, *args, **kwargs)
50+
except Exception:
51+
exc_info = sys.exc_info()
52+
_capture_exception(exc_info)
53+
reraise(*exc_info)
54+
55+
Router.handle_request = sentry_patched_handle_request
56+
57+
old_wsgi_call = Router.__call__
58+
59+
def sentry_patched_wsgi_call(self, environ, start_response):
60+
hub = Hub.current
61+
integration = hub.get_integration(PyramidIntegration)
62+
if integration is None:
63+
return old_wsgi_call(self, environ, start_response)
64+
65+
return run_wsgi_app(
66+
lambda *a, **kw: old_wsgi_call(self, *a, **kw), environ, start_response
67+
)
68+
69+
Router.__call__ = sentry_patched_wsgi_call
70+
71+
72+
def _capture_exception(exc_info, **kwargs):
73+
if issubclass(exc_info[0], HTTPException):
74+
return
75+
hub = Hub.current
76+
if hub.get_integration(PyramidIntegration) is None:
77+
return
78+
event, hint = event_from_exception(
79+
exc_info,
80+
client_options=hub.client.options,
81+
mechanism={"type": "pyramid", "handled": False},
82+
)
83+
84+
hub.capture_event(event, hint=hint)
85+
86+
87+
class PyramidRequestExtractor(RequestExtractor):
88+
def url(self):
89+
return self.request.path_url
90+
91+
def env(self):
92+
return self.request.environ
93+
94+
def cookies(self):
95+
return self.request.cookies
96+
97+
def raw_data(self):
98+
return self.request.text
99+
100+
def form(self):
101+
return {
102+
key: value
103+
for key, value in self.request.POST.items()
104+
if not getattr(value, "filename", None)
105+
}
106+
107+
def files(self):
108+
return {
109+
key: value
110+
for key, value in self.request.POST.items()
111+
if getattr(value, "filename", None)
112+
}
113+
114+
def size_of_file(self, postdata):
115+
file = postdata.file
116+
try:
117+
return os.fstat(file.fileno()).st_size
118+
except Exception:
119+
return 0
120+
121+
122+
def _make_event_processor(weak_request, integration):
123+
def event_processor(event, hint):
124+
request = weak_request()
125+
if request is None:
126+
return event
127+
128+
if "transaction" not in event:
129+
try:
130+
if integration.transaction_style == "route_name":
131+
event["transaction"] = request.matched_route.name
132+
elif integration.transaction_style == "route_pattern":
133+
event["transaction"] = request.matched_route.pattern
134+
except Exception:
135+
pass
136+
137+
with capture_internal_exceptions():
138+
PyramidRequestExtractor(request).extract_into_event(event)
139+
140+
if _should_send_default_pii():
141+
with capture_internal_exceptions():
142+
user_info = event.setdefault("user", {})
143+
if "id" not in user_info:
144+
user_info["id"] = request.authenticated_userid
145+
146+
return event
147+
148+
return event_processor

tests/integrations/flask/test_flask.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ def index():
290290
}
291291
assert len(event["request"]["data"]["foo"]) == 512
292292

293+
# TODO: wrong filesize reported due to usage of BytesIO. Use more realistic input
293294
assert event["_meta"]["request"]["data"]["file"] == {
294295
"": {"len": 0, "rem": [["!raw", "x", 0, 0]]}
295296
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pytest
2+
3+
pyramid = pytest.importorskip("pyramid")

0 commit comments

Comments
 (0)