diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/spotlight.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/sentry_sdk/spotlight.py | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/spotlight.py b/.venv/lib/python3.12/site-packages/sentry_sdk/spotlight.py new file mode 100644 index 00000000..c2473b77 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/sentry_sdk/spotlight.py @@ -0,0 +1,233 @@ +import io +import logging +import os +import urllib.parse +import urllib.request +import urllib.error +import urllib3 +import sys + +from itertools import chain, product + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + from typing import Callable + from typing import Dict + from typing import Optional + from typing import Self + +from sentry_sdk.utils import ( + logger as sentry_logger, + env_to_bool, + capture_internal_exceptions, +) +from sentry_sdk.envelope import Envelope + + +logger = logging.getLogger("spotlight") + + +DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" +DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" + + +class SpotlightClient: + def __init__(self, url): + # type: (str) -> None + self.url = url + self.http = urllib3.PoolManager() + self.tries = 0 + + def capture_envelope(self, envelope): + # type: (Envelope) -> None + body = io.BytesIO() + envelope.serialize_into(body) + try: + req = self.http.request( + url=self.url, + body=body.getvalue(), + method="POST", + headers={ + "Content-Type": "application/x-sentry-envelope", + }, + ) + req.close() + except Exception as e: + # TODO: Implement buffering and retrying with exponential backoff + sentry_logger.warning(str(e)) + + +try: + from django.utils.deprecation import MiddlewareMixin + from django.http import HttpResponseServerError, HttpResponse, HttpRequest + from django.conf import settings + + SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" + SPOTLIGHT_JS_SNIPPET_PATTERN = ( + "<script>window.__spotlight = {{ initOptions: {{ sidecarUrl: '{spotlight_url}', fullPage: false }} }};</script>\n" + '<script type="module" crossorigin src="{spotlight_js_url}"></script>\n' + ) + SPOTLIGHT_ERROR_PAGE_SNIPPET = ( + '<html><base href="{spotlight_url}">\n' + '<script>window.__spotlight = {{ initOptions: {{ fullPage: true, startFrom: "/errors/{event_id}" }}}};</script>\n' + ) + CHARSET_PREFIX = "charset=" + BODY_TAG_NAME = "body" + BODY_CLOSE_TAG_POSSIBILITIES = tuple( + "</{}>".format("".join(chars)) + for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower())) + ) + + class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] + _spotlight_script = None # type: Optional[str] + _spotlight_url = None # type: Optional[str] + + def __init__(self, get_response): + # type: (Self, Callable[..., HttpResponse]) -> None + super().__init__(get_response) + + import sentry_sdk.api + + self.sentry_sdk = sentry_sdk.api + + spotlight_client = self.sentry_sdk.get_client().spotlight + if spotlight_client is None: + sentry_logger.warning( + "Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware." + ) + return None + # Spotlight URL has a trailing `/stream` part at the end so split it off + self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") + + @property + def spotlight_script(self): + # type: (Self) -> Optional[str] + if self._spotlight_url is not None and self._spotlight_script is None: + try: + spotlight_js_url = urllib.parse.urljoin( + self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH + ) + req = urllib.request.Request( + spotlight_js_url, + method="HEAD", + ) + urllib.request.urlopen(req) + self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( + spotlight_url=self._spotlight_url, + spotlight_js_url=spotlight_js_url, + ) + except urllib.error.URLError as err: + sentry_logger.debug( + "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", + spotlight_js_url, + exc_info=err, + ) + + return self._spotlight_script + + def process_response(self, _request, response): + # type: (Self, HttpRequest, HttpResponse) -> Optional[HttpResponse] + content_type_header = tuple( + p.strip() + for p in response.headers.get("Content-Type", "").lower().split(";") + ) + content_type = content_type_header[0] + if len(content_type_header) > 1 and content_type_header[1].startswith( + CHARSET_PREFIX + ): + encoding = content_type_header[1][len(CHARSET_PREFIX) :] + else: + encoding = "utf-8" + + if ( + self.spotlight_script is not None + and not response.streaming + and content_type == "text/html" + ): + content_length = len(response.content) + injection = self.spotlight_script.encode(encoding) + injection_site = next( + ( + idx + for idx in ( + response.content.rfind(body_variant.encode(encoding)) + for body_variant in BODY_CLOSE_TAG_POSSIBILITIES + ) + if idx > -1 + ), + content_length, + ) + + # This approach works even when we don't have a `</body>` tag + response.content = ( + response.content[:injection_site] + + injection + + response.content[injection_site:] + ) + + if response.has_header("Content-Length"): + response.headers["Content-Length"] = content_length + len(injection) + + return response + + def process_exception(self, _request, exception): + # type: (Self, HttpRequest, Exception) -> Optional[HttpResponseServerError] + if not settings.DEBUG or not self._spotlight_url: + return None + + try: + spotlight = ( + urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") + ) + except urllib.error.URLError: + return None + else: + event_id = self.sentry_sdk.capture_exception(exception) + return HttpResponseServerError( + spotlight.replace( + "<html>", + SPOTLIGHT_ERROR_PAGE_SNIPPET.format( + spotlight_url=self._spotlight_url, event_id=event_id + ), + ) + ) + +except ImportError: + settings = None + + +def setup_spotlight(options): + # type: (Dict[str, Any]) -> Optional[SpotlightClient] + _handler = logging.StreamHandler(sys.stderr) + _handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s")) + logger.addHandler(_handler) + logger.setLevel(logging.INFO) + + url = options.get("spotlight") + + if url is True: + url = DEFAULT_SPOTLIGHT_URL + + if not isinstance(url, str): + return None + + with capture_internal_exceptions(): + if ( + settings is not None + and settings.DEBUG + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")) + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1")) + ): + middleware = settings.MIDDLEWARE + if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware: + settings.MIDDLEWARE = type(middleware)( + chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) + ) + logger.info("Enabled Spotlight integration for Django") + + client = SpotlightClient(url) + logger.info("Enabled Spotlight using sidecar at %s", url) + + return client |