27
27
from sanic .request import Request , RequestParameters
28
28
29
29
from sentry_sdk ._types import Event , EventProcessor , Hint
30
+ from sanic .router import Route
30
31
31
32
try :
32
33
from sanic import Sanic , __version__ as SANIC_VERSION
36
37
except ImportError :
37
38
raise DidNotEnable ("Sanic not installed" )
38
39
40
+ old_error_handler_lookup = ErrorHandler .lookup
41
+ old_handle_request = Sanic .handle_request
42
+ old_router_get = Router .get
43
+
44
+ try :
45
+ # This method was introduced in Sanic v21.9
46
+ old_startup = Sanic ._startup
47
+ except AttributeError :
48
+ pass
49
+
39
50
40
51
class SanicIntegration (Integration ):
41
52
identifier = "sanic"
53
+ version = (0 , 0 ) # type: Tuple[int, ...]
42
54
43
55
@staticmethod
44
56
def setup_once ():
45
57
# type: () -> None
58
+
46
59
try :
47
- version = tuple (map (int , SANIC_VERSION .split ("." )))
60
+ SanicIntegration . version = tuple (map (int , SANIC_VERSION .split ("." )))
48
61
except (TypeError , ValueError ):
49
62
raise DidNotEnable ("Unparsable Sanic version: {}" .format (SANIC_VERSION ))
50
63
51
- if version < (0 , 8 ):
64
+ if SanicIntegration . version < (0 , 8 ):
52
65
raise DidNotEnable ("Sanic 0.8 or newer required." )
53
66
54
67
if not HAS_REAL_CONTEXTVARS :
@@ -71,89 +84,194 @@ def setup_once():
71
84
# https://github.com/huge-success/sanic/issues/1332
72
85
ignore_logger ("root" )
73
86
74
- old_handle_request = Sanic .handle_request
87
+ if SanicIntegration .version < (21 , 9 ):
88
+ _setup_legacy_sanic ()
89
+ return
75
90
76
- async def sentry_handle_request (self , request , * args , ** kwargs ):
77
- # type: (Any, Request, *Any, **Any) -> Any
78
- hub = Hub .current
79
- if hub .get_integration (SanicIntegration ) is None :
80
- return old_handle_request (self , request , * args , ** kwargs )
91
+ _setup_sanic ()
81
92
82
- weak_request = weakref .ref (request )
83
93
84
- with Hub (hub ) as hub :
85
- with hub .configure_scope () as scope :
86
- scope .clear_breadcrumbs ()
87
- scope .add_event_processor (_make_request_processor (weak_request ))
94
+ class SanicRequestExtractor (RequestExtractor ):
95
+ def content_length (self ):
96
+ # type: () -> int
97
+ if self .request .body is None :
98
+ return 0
99
+ return len (self .request .body )
88
100
89
- response = old_handle_request (self , request , * args , ** kwargs )
90
- if isawaitable ( response ):
91
- response = await response
101
+ def cookies (self ):
102
+ # type: () -> Dict[str, str]
103
+ return dict ( self . request . cookies )
92
104
93
- return response
105
+ def raw_data (self ):
106
+ # type: () -> bytes
107
+ return self .request .body
94
108
95
- Sanic .handle_request = sentry_handle_request
109
+ def form (self ):
110
+ # type: () -> RequestParameters
111
+ return self .request .form
96
112
97
- old_router_get = Router .get
113
+ def is_json (self ):
114
+ # type: () -> bool
115
+ raise NotImplementedError ()
98
116
99
- def sentry_router_get (self , * args ):
100
- # type: (Any, Union[Any, Request]) -> Any
101
- rv = old_router_get (self , * args )
102
- hub = Hub .current
103
- if hub .get_integration (SanicIntegration ) is not None :
104
- with capture_internal_exceptions ():
105
- with hub .configure_scope () as scope :
106
- if version >= (21 , 3 ):
107
- # Sanic versions above and including 21.3 append the app name to the
108
- # route name, and so we need to remove it from Route name so the
109
- # transaction name is consistent across all versions
110
- sanic_app_name = self .ctx .app .name
111
- sanic_route = rv [0 ].name
117
+ def json (self ):
118
+ # type: () -> Optional[Any]
119
+ return self .request .json
112
120
113
- if sanic_route .startswith ("%s." % sanic_app_name ):
114
- # We add a 1 to the len of the sanic_app_name because there is a dot
115
- # that joins app name and the route name
116
- # Format: app_name.route_name
117
- sanic_route = sanic_route [len (sanic_app_name ) + 1 :]
121
+ def files (self ):
122
+ # type: () -> RequestParameters
123
+ return self .request .files
124
+
125
+ def size_of_file (self , file ):
126
+ # type: (Any) -> int
127
+ return len (file .body or ())
118
128
119
- scope .transaction = sanic_route
120
- else :
121
- scope .transaction = rv [0 ].__name__
122
- return rv
123
129
124
- Router .get = sentry_router_get
130
+ def _setup_sanic ():
131
+ # type: () -> None
132
+ Sanic ._startup = _startup
133
+ ErrorHandler .lookup = _sentry_error_handler_lookup
125
134
126
- old_error_handler_lookup = ErrorHandler .lookup
127
135
128
- def sentry_error_handler_lookup (self , exception ):
129
- # type: (Any, Exception) -> Optional[object]
130
- _capture_exception (exception )
131
- old_error_handler = old_error_handler_lookup (self , exception )
136
+ def _setup_legacy_sanic ():
137
+ # type: () -> None
138
+ Sanic .handle_request = _legacy_handle_request
139
+ Router .get = _legacy_router_get
140
+ ErrorHandler .lookup = _sentry_error_handler_lookup
132
141
133
- if old_error_handler is None :
134
- return None
135
142
136
- if Hub .current .get_integration (SanicIntegration ) is None :
137
- return old_error_handler
143
+ async def _startup (self ):
144
+ # type: (Sanic) -> None
145
+ # This happens about as early in the lifecycle as possible, just after the
146
+ # Request object is created. The body has not yet been consumed.
147
+ self .signal ("http.lifecycle.request" )(_hub_enter )
148
+
149
+ # This happens after the handler is complete. In v21.9 this signal is not
150
+ # dispatched when there is an exception. Therefore we need to close out
151
+ # and call _hub_exit from the custom exception handler as well.
152
+ # See https://github.com/sanic-org/sanic/issues/2297
153
+ self .signal ("http.lifecycle.response" )(_hub_exit )
154
+
155
+ # This happens inside of request handling immediately after the route
156
+ # has been identified by the router.
157
+ self .signal ("http.routing.after" )(_set_transaction )
158
+
159
+ # The above signals need to be declared before this can be called.
160
+ await old_startup (self )
161
+
162
+
163
+ async def _hub_enter (request ):
164
+ # type: (Request) -> None
165
+ hub = Hub .current
166
+ request .ctx ._sentry_do_integration = (
167
+ hub .get_integration (SanicIntegration ) is not None
168
+ )
169
+
170
+ if not request .ctx ._sentry_do_integration :
171
+ return
172
+
173
+ weak_request = weakref .ref (request )
174
+ request .ctx ._sentry_hub = Hub (hub )
175
+ request .ctx ._sentry_hub .__enter__ ()
176
+
177
+ with request .ctx ._sentry_hub .configure_scope () as scope :
178
+ scope .clear_breadcrumbs ()
179
+ scope .add_event_processor (_make_request_processor (weak_request ))
180
+
181
+
182
+ async def _hub_exit (request , ** _ ):
183
+ # type: (Request, **Any) -> None
184
+ request .ctx ._sentry_hub .__exit__ (None , None , None )
185
+
186
+
187
+ async def _set_transaction (request , route , ** kwargs ):
188
+ # type: (Request, Route, **Any) -> None
189
+ hub = Hub .current
190
+ if hub .get_integration (SanicIntegration ) is not None :
191
+ with capture_internal_exceptions ():
192
+ with hub .configure_scope () as scope :
193
+ route_name = route .name .replace (request .app .name , "" ).strip ("." )
194
+ scope .transaction = route_name
138
195
139
- async def sentry_wrapped_error_handler (request , exception ):
140
- # type: (Request, Exception) -> Any
141
- try :
142
- response = old_error_handler (request , exception )
143
- if isawaitable (response ):
144
- response = await response
145
- return response
146
- except Exception :
147
- # Report errors that occur in Sanic error handler. These
148
- # exceptions will not even show up in Sanic's
149
- # `sanic.exceptions` logger.
150
- exc_info = sys .exc_info ()
151
- _capture_exception (exc_info )
152
- reraise (* exc_info )
153
196
154
- return sentry_wrapped_error_handler
197
+ def _sentry_error_handler_lookup (self , exception , * args , ** kwargs ):
198
+ # type: (Any, Exception, *Any, **Any) -> Optional[object]
199
+ _capture_exception (exception )
200
+ old_error_handler = old_error_handler_lookup (self , exception , * args , ** kwargs )
155
201
156
- ErrorHandler .lookup = sentry_error_handler_lookup
202
+ if old_error_handler is None :
203
+ return None
204
+
205
+ if Hub .current .get_integration (SanicIntegration ) is None :
206
+ return old_error_handler
207
+
208
+ async def sentry_wrapped_error_handler (request , exception ):
209
+ # type: (Request, Exception) -> Any
210
+ try :
211
+ response = old_error_handler (request , exception )
212
+ if isawaitable (response ):
213
+ response = await response
214
+ return response
215
+ except Exception :
216
+ # Report errors that occur in Sanic error handler. These
217
+ # exceptions will not even show up in Sanic's
218
+ # `sanic.exceptions` logger.
219
+ exc_info = sys .exc_info ()
220
+ _capture_exception (exc_info )
221
+ reraise (* exc_info )
222
+ finally :
223
+ # As mentioned in previous comment in _startup, this can be removed
224
+ # after https://github.com/sanic-org/sanic/issues/2297 is resolved
225
+ if SanicIntegration .version >= (21 , 9 ):
226
+ await _hub_exit (request )
227
+
228
+ return sentry_wrapped_error_handler
229
+
230
+
231
+ async def _legacy_handle_request (self , request , * args , ** kwargs ):
232
+ # type: (Any, Request, *Any, **Any) -> Any
233
+ hub = Hub .current
234
+ if hub .get_integration (SanicIntegration ) is None :
235
+ return old_handle_request (self , request , * args , ** kwargs )
236
+
237
+ weak_request = weakref .ref (request )
238
+
239
+ with Hub (hub ) as hub :
240
+ with hub .configure_scope () as scope :
241
+ scope .clear_breadcrumbs ()
242
+ scope .add_event_processor (_make_request_processor (weak_request ))
243
+
244
+ response = old_handle_request (self , request , * args , ** kwargs )
245
+ if isawaitable (response ):
246
+ response = await response
247
+
248
+ return response
249
+
250
+
251
+ def _legacy_router_get (self , * args ):
252
+ # type: (Any, Union[Any, Request]) -> Any
253
+ rv = old_router_get (self , * args )
254
+ hub = Hub .current
255
+ if hub .get_integration (SanicIntegration ) is not None :
256
+ with capture_internal_exceptions ():
257
+ with hub .configure_scope () as scope :
258
+ if SanicIntegration .version and SanicIntegration .version >= (21 , 3 ):
259
+ # Sanic versions above and including 21.3 append the app name to the
260
+ # route name, and so we need to remove it from Route name so the
261
+ # transaction name is consistent across all versions
262
+ sanic_app_name = self .ctx .app .name
263
+ sanic_route = rv [0 ].name
264
+
265
+ if sanic_route .startswith ("%s." % sanic_app_name ):
266
+ # We add a 1 to the len of the sanic_app_name because there is a dot
267
+ # that joins app name and the route name
268
+ # Format: app_name.route_name
269
+ sanic_route = sanic_route [len (sanic_app_name ) + 1 :]
270
+
271
+ scope .transaction = sanic_route
272
+ else :
273
+ scope .transaction = rv [0 ].__name__
274
+ return rv
157
275
158
276
159
277
def _capture_exception (exception ):
@@ -211,39 +329,3 @@ def sanic_processor(event, hint):
211
329
return event
212
330
213
331
return sanic_processor
214
-
215
-
216
- class SanicRequestExtractor (RequestExtractor ):
217
- def content_length (self ):
218
- # type: () -> int
219
- if self .request .body is None :
220
- return 0
221
<
D7AE
/td>
- return len (self .request .body )
222
-
223
- def cookies (self ):
224
- # type: () -> Dict[str, str]
225
- return dict (self .request .cookies )
226
-
227
- def raw_data (self ):
228
- # type: () -> bytes
229
- return self .request .body
230
-
231
- def form (self ):
232
- # type: () -> RequestParameters
233
- return self .request .form
234
-
235
- def is_json (self ):
236
- # type: () -> bool
237
- raise NotImplementedError ()
238
-
239
- def json (self ):
240
- # type: () -> Optional[Any]
241
- return self .request .json
242
-
243
- def files (self ):
244
- # type: () -> RequestParameters
245
- return self .request .files
246
-
247
- def size_of_file (self , file ):
248
- # type: (Any) -> int
249
- return len (file .body or ())
0 commit comments