2727 from sanic .request import Request , RequestParameters
2828
2929 from sentry_sdk ._types import Event , EventProcessor , Hint
30+ from sanic .router import Route
3031
3132try :
3233 from sanic import Sanic , __version__ as SANIC_VERSION
3637except ImportError :
3738 raise DidNotEnable ("Sanic not installed" )
3839
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+
3950
4051class SanicIntegration (Integration ):
4152 identifier = "sanic"
53+ version = (0 , 0 ) # type: Tuple[int, ...]
4254
4355 @staticmethod
4456 def setup_once ():
4557 # type: () -> None
58+
4659 try :
47- version = tuple (map (int , SANIC_VERSION .split ("." )))
60+ SanicIntegration . version = tuple (map (int , SANIC_VERSION .split ("." )))
4861 except (TypeError , ValueError ):
4962 raise DidNotEnable ("Unparsable Sanic version: {}" .format (SANIC_VERSION ))
5063
51- if version < (0 , 8 ):
64+ if SanicIntegration . version < (0 , 8 ):
5265 raise DidNotEnable ("Sanic 0.8 or newer required." )
5366
5467 if not HAS_REAL_CONTEXTVARS :
@@ -71,89 +84,194 @@ def setup_once():
7184 # https://github.com/huge-success/sanic/issues/1332
7285 ignore_logger ("root" )
7386
74- old_handle_request = Sanic .handle_request
87+ if SanicIntegration .version < (21 , 9 ):
88+ _setup_legacy_sanic ()
89+ return
7590
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 ()
8192
82- weak_request = weakref .ref (request )
8393
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 )
88100
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 )
92104
93- return response
105+ def raw_data (self ):
106+ # type: () -> bytes
107+ return self .request .body
94108
95- Sanic .handle_request = sentry_handle_request
109+ def form (self ):
110+ # type: () -> RequestParameters
111+ return self .request .form
96112
97- old_router_get = Router .get
113+ def is_json (self ):
114+ # type: () -> bool
115+ raise NotImplementedError ()
98116
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
112120
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 ())
118128
119- scope .transaction = sanic_route
120- else :
121- scope .transaction = rv [0 ].__name__
122- return rv
123129
124- Router .get = sentry_router_get
130+ def _setup_sanic ():
131+ # type: () -> None
132+ Sanic ._startup = _startup
133+ ErrorHandler .lookup = _sentry_error_handler_lookup
125134
126- old_error_handler_lookup = ErrorHandler .lookup
127135
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
132141
133- if old_error_handler is None :
134- return None
135142
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
138195
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 )
153196
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 )
155201
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
157275
158276
159277def _capture_exception (exception ):
@@ -211,39 +329,3 @@ def sanic_processor(event, hint):
211329 return event
212330
213331 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- 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