1212from sentry_sdk ._types import MYPY
1313from sentry_sdk .hub import Hub , _should_send_default_pii
1414from sentry_sdk .integrations ._wsgi_common import _filter_headers
15- from sentry_sdk .utils import ContextVar , event_from_exception , transaction_from_function
15+ from sentry_sdk .utils import (
16+ ContextVar ,
17+ event_from_exception ,
18+ transaction_from_function ,
19+ HAS_REAL_CONTEXTVARS ,
20+ CONTEXTVARS_ERROR_MESSAGE ,
21+ )
1622from sentry_sdk .tracing import Span
1723
1824if MYPY :
2127 from typing import Optional
2228 from typing import Callable
2329
30+ from typing_extensions import Literal
31+
2432 from sentry_sdk ._types import Event , Hint
2533
2634
2735_asgi_middleware_applied = ContextVar ("sentry_asgi_middleware_applied" )
2836
37+ _DEFAULT_TRANSACTION_NAME = "generic ASGI request"
38+
2939
3040def _capture_exception (hub , exc ):
3141 # type: (Hub, Any) -> None
@@ -59,8 +69,23 @@ def _looks_like_asgi3(app):
5969class SentryAsgiMiddleware :
6070 __slots__ = ("app" , "__call__" )
6171
62- def __init__ (self , app ):
63- # type: (Any) -> None
72+ def __init__ (self , app , unsafe_context_data = False ):
73+ # type: (Any, bool) -> None
74+ """
75+ Instrument an ASGI application with Sentry. Provides HTTP/websocket
76+ data to sent events and basic handling for exceptions bubbling up
77+ through the middleware.
78+
79+ :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default.
80+ """
81+
82+ if not unsafe_context_data and not HAS_REAL_CONTEXTVARS :
83+ # We better have contextvars or we're going to leak state between
84+ # requests.
85+ raise RuntimeError (
86+ "The ASGI middleware for Sentry requires Python 3.7+ "
87+ "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE
88+ )
6489 self .app = app
6590
6691 if _looks_like_asgi3 (app ):
@@ -95,15 +120,17 @@ async def _run_app(self, scope, callback):
95120 processor = partial (self .event_processor , asgi_scope = scope )
96121 sentry_scope .add_event_processor (processor )
97122
98- if scope ["type" ] in ("http" , "websocket" ):
123+ ty = scope ["type" ]
124+
125+ if ty in ("http" , "websocket" ):
99126 span = Span .continue_from_headers (dict (scope ["headers" ]))
100- span .op = "{}.server" .format (scope [ "type" ] )
127+ span .op = "{}.server" .format (ty )
101128 else :
102129 span = Span ()
103130 span .op = "asgi.server"
104131
105- span .set_tag ("asgi.type" , scope [ "type" ] )
106- span .transaction = "generic ASGI request"
132+ span .set_tag ("asgi.type" , ty )
133+ span .transaction = _DEFAULT_TRANSACTION_NAME
107134
108135 with hub .start_span (span ) as span :
109136 # XXX: Would be cool to have correct span status, but we
@@ -121,38 +148,55 @@ def event_processor(self, event, hint, asgi_scope):
121148 # type: (Event, Hint, Any) -> Optional[Event]
122149 request_info = event .get ("request" , {})
123150
124- if asgi_scope ["type" ] in ("http" , "websocket" ):
125- request_info ["url" ] = self .get_url (asgi_scope )
126- request_info ["method" ] = asgi_scope ["method" ]
127- request_info ["headers" ] = _filter_headers (self .get_headers (asgi_scope ))
128- request_info ["query_string" ] = self .get_query (asgi_scope )
129-
130- if asgi_scope .get ("client" ) and _should_send_default_pii ():
131- request_info ["env" ] = {"REMOTE_ADDR" : asgi_scope ["client" ][0 ]}
132-
133- if asgi_scope .get ("endpoint" ):
151+ ty = asgi_scope ["type" ]
152+ if ty in ("http" , "websocket" ):
153+ request_info ["method" ] = asgi_scope .get ("method" )
154+ request_info ["headers" ] = headers = _filter_headers (
155+ self ._get_headers (asgi_scope )
156+ )
157+ request_info ["query_string" ] = self ._get_query (asgi_scope )
158+
159+ request_info ["url" ] = self ._get_url (
160+ asgi_scope , "http" if ty == "http" else "ws" , headers .get ("host" )
161+ )
162+
163+ client = asgi_scope .get ("client" )
164+ if client and _should_send_default_pii ():
165+ request_info ["env" ] = {"REMOTE_ADDR" : client [0 ]}
166+
167+ if (
168+ event .get ("transaction" , _DEFAULT_TRANSACTION_NAME )
169+ == _DEFAULT_TRANSACTION_NAME
170+ ):
171+ endpoint = asgi_scope .get ("endpoint" )
134172 # Webframeworks like Starlette mutate the ASGI env once routing is
135173 # done, which is sometime after the request has started. If we have
136- # an endpoint, overwrite our path-based transaction name.
137- event ["transaction" ] = self .get_transaction (asgi_scope )
174+ # an endpoint, overwrite our generic transaction name.
175+ if endpoint :
176+ event ["transaction" ] = transaction_from_function (endpoint )
138177
139178 event ["request" ] = request_info
140179
141180 return event
142181
143- def get_url (self , scope ):
144- # type: (Any) -> str
182+ # Helper functions for extracting request data.
183+ #
184+ # Note: Those functions are not public API. If you want to mutate request
185+ # data to your liking it's recommended to use the `before_send` callback
186+ # for that.
187+
188+ def _get_url (self , scope , default_scheme , host ):
189+ # type: (Dict[str, Any], Literal["ws", "http"], Optional[str]) -> str
145190 """
146191 Extract URL from the ASGI scope, without also including the querystring.
147192 """
148- scheme = scope .get ("scheme" , "http" )
193+ scheme = scope .get ("scheme" , default_scheme )
194+
149195 server = scope .get ("server" , None )
150- path = scope .get ("root_path" , "" ) + scope [ "path" ]
196+ path = scope .get ("root_path" , "" ) + scope . get ( "path" , "" )
151197
152- for key , value in scope ["headers" ]:
153- if key == b"host" :
154- host_header = value .decode ("latin-1" )
155- return "%s://%s%s" % (scheme , host_header , path )
198+ if host :
199+ return "%s://%s%s" % (scheme , host , path )
156200
157201 if server is not None :
158202 host , port = server
@@ -162,15 +206,18 @@ def get_url(self, scope):
162206 return "%s://%s%s" % (scheme , host , path )
163207 return path
164208
165- def get_query (self , scope ):
209+ def _get_query (self , scope ):
166210 # type: (Any) -> Any
167211 """
168212 Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
169213 """
170- return urllib .parse .unquote (scope ["query_string" ].decode ("latin-1" ))
214+ qs = scope .get ("query_string" )
215+ if not qs :
216+ return None
217+ return urllib .parse .unquote (qs .decode ("latin-1" ))
171218
172- def get_headers (self , scope ):
173- # type: (Any) -> Dict[str, Any ]
219+ def _get_headers (self , scope ):
220+ # type: (Any) -> Dict[str, str ]
174221 """
175222 Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
176223 """
@@ -183,10 +230,3 @@ def get_headers(self, scope):
183230 else :
184231 headers [key ] = value
185232 return headers
186-
187- def get_transaction (self , scope ):
188- # type: (Any) -> Optional[str]
189- """
190- Return a transaction string to identify the routed endpoint.
191- """
192- return transaction_from_function (scope ["endpoint" ])
0 commit comments