Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/integrations/wsgi.py: 17%
163 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import sys
3from sentry_sdk._functools import partial
4from sentry_sdk.hub import Hub, _should_send_default_pii
5from sentry_sdk.utils import (
6 ContextVar,
7 capture_internal_exceptions,
8 event_from_exception,
9)
10from sentry_sdk._compat import PY2, reraise, iteritems
11from sentry_sdk.tracing import Transaction
12from sentry_sdk.sessions import auto_session_tracking
13from sentry_sdk.integrations._wsgi_common import _filter_headers
15from sentry_sdk._types import MYPY
17if MYPY: 17 ↛ 18line 17 didn't jump to line 18, because the condition on line 17 was never true
18 from typing import Callable
19 from typing import Dict
20 from typing import Iterator
21 from typing import Any
22 from typing import Tuple
23 from typing import Optional
24 from typing import TypeVar
25 from typing import Protocol
27 from sentry_sdk.utils import ExcInfo
28 from sentry_sdk._types import EventProcessor
30 WsgiResponseIter = TypeVar("WsgiResponseIter")
31 WsgiResponseHeaders = TypeVar("WsgiResponseHeaders")
32 WsgiExcInfo = TypeVar("WsgiExcInfo")
34 class StartResponse(Protocol):
35 def __call__(self, status, response_headers, exc_info=None):
36 # type: (str, WsgiResponseHeaders, Optional[WsgiExcInfo]) -> WsgiResponseIter
37 pass
40_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
43if PY2: 43 ↛ 45line 43 didn't jump to line 45, because the condition on line 43 was never true
45 def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
46 # type: (str, str, str) -> str
47 return s.decode(charset, errors)
49else:
51 def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
52 # type: (str, str, str) -> str
53 return s.encode("latin1").decode(charset, errors)
56def get_host(environ, use_x_forwarded_for=False):
57 # type: (Dict[str, str], bool) -> str
58 """Return the host for the given WSGI environment. Yanked from Werkzeug."""
59 if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ:
60 rv = environ["HTTP_X_FORWARDED_HOST"]
61 if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
62 rv = rv[:-3]
63 elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
64 rv = rv[:-4]
65 elif environ.get("HTTP_HOST"):
66 rv = environ["HTTP_HOST"]
67 if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
68 rv = rv[:-3]
69 elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
70 rv = rv[:-4]
71 elif environ.get("SERVER_NAME"):
72 rv = environ["SERVER_NAME"]
73 if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in (
74 ("https", "443"),
75 ("http", "80"),
76 ):
77 rv += ":" + environ["SERVER_PORT"]
78 else:
79 # In spite of the WSGI spec, SERVER_NAME might not be present.
80 rv = "unknown"
82 return rv
85def get_request_url(environ, use_x_forwarded_for=False):
86 # type: (Dict[str, str], bool) -> str
87 """Return the absolute URL without query string for the given WSGI
88 environment."""
89 return "%s://%s/%s" % (
90 environ.get("wsgi.url_scheme"),
91 get_host(environ, use_x_forwarded_for),
92 wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"),
93 )
96class SentryWsgiMiddleware(object):
97 __slots__ = ("app", "use_x_forwarded_for")
99 def __init__(self, app, use_x_forwarded_for=False):
100 # type: (Callable[[Dict[str, str], Callable[..., Any]], Any], bool) -> None
101 self.app = app
102 self.use_x_forwarded_for = use_x_forwarded_for
104 def __call__(self, environ, start_response):
105 # type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse
106 if _wsgi_middleware_applied.get(False):
107 return self.app(environ, start_response)
109 _wsgi_middleware_applied.set(True)
110 try:
111 hub = Hub(Hub.current)
112 with auto_session_tracking(hub, session_mode="request"):
113 with hub:
114 with capture_internal_exceptions():
115 with hub.configure_scope() as scope:
116 scope.clear_breadcrumbs()
117 scope._name = "wsgi"
118 scope.add_event_processor(
119 _make_wsgi_event_processor(
120 environ, self.use_x_forwarded_for
121 )
122 )
124 transaction = Transaction.continue_from_environ(
125 environ, op="http.server", name="generic WSGI request"
126 )
128 with hub.start_transaction(
129 transaction, custom_sampling_context={"wsgi_environ": environ}
130 ):
131 try:
132 rv = self.app(
133 environ,
134 partial(
135 _sentry_start_response, start_response, transaction
136 ),
137 )
138 except BaseException:
139 reraise(*_capture_exception(hub))
140 finally:
141 _wsgi_middleware_applied.set(False)
143 return _ScopedResponse(hub, rv)
146def _sentry_start_response(
147 old_start_response, # type: StartResponse
148 transaction, # type: Transaction
149 status, # type: str
150 response_headers, # type: WsgiResponseHeaders
151 exc_info=None, # type: Optional[WsgiExcInfo]
152):
153 # type: (...) -> WsgiResponseIter
154 with capture_internal_exceptions():
155 status_int = int(status.split(" ", 1)[0])
156 transaction.set_http_status(status_int)
158 if exc_info is None:
159 # The Django Rest Framework WSGI test client, and likely other
160 # (incorrect) implementations, cannot deal with the exc_info argument
161 # if one is present. Avoid providing a third argument if not necessary.
162 return old_start_response(status, response_headers)
163 else:
164 return old_start_response(status, response_headers, exc_info)
167def _get_environ(environ):
168 # type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
169 """
170 Returns our explicitly included environment variables we want to
171 capture (server name, port and remote addr if pii is enabled).
172 """
173 keys = ["SERVER_NAME", "SERVER_PORT"]
174 if _should_send_default_pii():
175 # make debugging of proxy setup easier. Proxy headers are
176 # in headers.
177 keys += ["REMOTE_ADDR"]
179 for key in keys:
180 if key in environ:
181 yield key, environ[key]
184# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
185#
186# We need this function because Django does not give us a "pure" http header
187# dict. So we might as well use it for all WSGI integrations.
188def _get_headers(environ):
189 # type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
190 """
191 Returns only proper HTTP headers.
193 """
194 for key, value in iteritems(environ):
195 key = str(key)
196 if key.startswith("HTTP_") and key not in (
197 "HTTP_CONTENT_TYPE",
198 "HTTP_CONTENT_LENGTH",
199 ):
200 yield key[5:].replace("_", "-").title(), value
201 elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
202 yield key.replace("_", "-").title(), value
205def get_client_ip(environ):
206 # type: (Dict[str, str]) -> Optional[Any]
207 """
208 Infer the user IP address from various headers. This cannot be used in
209 security sensitive situations since the value may be forged from a client,
210 but it's good enough for the event payload.
211 """
212 try:
213 return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip()
214 except (KeyError, IndexError):
215 pass
217 try:
218 return environ["HTTP_X_REAL_IP"]
219 except KeyError:
220 pass
222 return environ.get("REMOTE_ADDR")
225def _capture_exception(hub):
226 # type: (Hub) -> ExcInfo
227 exc_info = sys.exc_info()
229 # Check client here as it might have been unset while streaming response
230 if hub.client is not None:
231 e = exc_info[1]
233 # SystemExit(0) is the only uncaught exception that is expected behavior
234 should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None)
235 if not should_skip_capture:
236 event, hint = event_from_exception(
237 exc_info,
238 client_options=hub.client.options,
239 mechanism={"type": "wsgi", "handled": False},
240 )
241 hub.capture_event(event, hint=hint)
243 return exc_info
246class _ScopedResponse(object):
247 __slots__ = ("_response", "_hub")
249 def __init__(self, hub, response):
250 # type: (Hub, Iterator[bytes]) -> None
251 self._hub = hub
252 self._response = response
254 def __iter__(self):
255 # type: () -> Iterator[bytes]
256 iterator = iter(self._response)
258 while True:
259 with self._hub:
260 try:
261 chunk = next(iterator)
262 except StopIteration:
263 break
264 except BaseException:
265 reraise(*_capture_exception(self._hub))
267 yield chunk
269 def close(self):
270 # type: () -> None
271 with self._hub:
272 try:
273 self._response.close() # type: ignore
274 except AttributeError:
275 pass
276 except BaseException:
277 reraise(*_capture_exception(self._hub))
280def _make_wsgi_event_processor(environ, use_x_forwarded_for):
281 # type: (Dict[str, str], bool) -> EventProcessor
282 # It's a bit unfortunate that we have to extract and parse the request data
283 # from the environ so eagerly, but there are a few good reasons for this.
284 #
285 # We might be in a situation where the scope/hub never gets torn down
286 # properly. In that case we will have an unnecessary strong reference to
287 # all objects in the environ (some of which may take a lot of memory) when
288 # we're really just interested in a few of them.
289 #
290 # Keeping the environment around for longer than the request lifecycle is
291 # also not necessarily something uWSGI can deal with:
292 # https://github.com/unbit/uwsgi/issues/1950
294 client_ip = get_client_ip(environ)
295 request_url = get_request_url(environ, use_x_forwarded_for)
296 query_string = environ.get("QUERY_STRING")
297 method = environ.get("REQUEST_METHOD")
298 env = dict(_get_environ(environ))
299 headers = _filter_headers(dict(_get_headers(environ)))
301 def event_processor(event, hint):
302 # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
303 with capture_internal_exceptions():
304 # if the code below fails halfway through we at least have some data
305 request_info = event.setdefault("request", {})
307 if _should_send_default_pii():
308 user_info = event.setdefault("user", {})
309 if client_ip:
310 user_info.setdefault("ip_address", client_ip)
312 request_info["url"] = request_url
313 request_info["query_string"] = query_string
314 request_info["method"] = method
315 request_info["env"] = env
316 request_info["headers"] = headers
318 return event
320 return event_processor