From 6a6881e5732d0f85675c5f5119f591dad7dfbad9 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 8 Nov 2024 12:34:43 +0000 Subject: [PATCH 01/10] feat(spotlight): Inject Spotlight button on Django This patch expands the `SpotlightMiddleware` for Django and injects the Spotlight button to all HTML responses when Spotlight is enabled and running. It requires Spotlight 2.6.0 to work this way. --- sentry_sdk/spotlight.py | 151 +++++++++++++++++++++++++++++++++------- 1 file changed, 125 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index e7e90f9822..98298a9fa1 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -6,7 +6,7 @@ import urllib.error import urllib3 -from itertools import chain +from itertools import chain, product from typing import TYPE_CHECKING @@ -16,7 +16,11 @@ from typing import Dict from typing import Optional -from sentry_sdk.utils import logger, env_to_bool, capture_internal_exceptions +from sentry_sdk.utils import ( + logger as sentry_logger, + env_to_bool, + capture_internal_exceptions, +) from sentry_sdk.envelope import Envelope @@ -34,7 +38,7 @@ def __init__(self, url): def capture_envelope(self, envelope): # type: (Envelope) -> None if self.tries > 3: - logger.warning( + sentry_logger.warning( "Too many errors sending to Spotlight, stop sending events there." ) return @@ -52,50 +56,144 @@ def capture_envelope(self, envelope): req.close() except Exception as e: self.tries += 1 - logger.warning(str(e)) + sentry_logger.warning(str(e)) try: - from django.http import HttpResponseServerError - from django.conf import settings + from typing import Self, Optional - class SpotlightMiddleware: - def __init__(self, get_response): - # type: (Any, Callable[..., Any]) -> None - self.get_response = get_response + from django.utils.deprecation import MiddlewareMixin + from django.http import HttpResponseServerError, HttpResponse, HttpRequest + from django.conf import settings - def __call__(self, request): - # type: (Any, Any) -> Any - return self.get_response(request) + SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" + SPOTLIGHT_JS_SNIPPET_PATTERN = ( + '' + ) + SPOTLIGHT_ERROR_PAGE_SNIPPET = ( + '\n' + '\n' + ) + CHARSET_PREFIX = "charset=" + BODY_CLOSE_TAG = "" + BODY_CLOSE_TAG_POSSIBILITIES = [ + "".join(l) + for l in product(*zip(BODY_CLOSE_TAG.upper(), BODY_CLOSE_TAG.lower())) + ] + + class SpotlightMiddleware(MiddlewareMixin): + _spotlight_script: Optional[str] + _spotlight_url: str - def process_exception(self, _request, exception): - # type: (Any, Any, Exception) -> Optional[HttpResponseServerError] - if not settings.DEBUG: - return None + 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 = sentry_sdk.api.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 - spotlight_url = spotlight_client.url.rsplit("/", 1)[0] + spotlight_url = self._spotlight_url = urllib.parse.urljoin( + spotlight_client.url, "../" + ) try: - spotlight = urllib.request.urlopen(spotlight_url).read().decode("utf-8") + spotlight_js_url = urllib.parse.urljoin( + spotlight_url, SPOTLIGHT_JS_ENTRY_PATH + ) + req = urllib.request.Request( + spotlight_js_url, + method="HEAD", + ) + status_code = urllib.request.urlopen(req).status + if status_code >= 200 and status_code < 400: + self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( + spotlight_js_url + ) + else: + sentry_logger.debug( + "Could not get Spotlight JS from %s (status: %s), SpotlightMiddleware will not be useful.", + spotlight_js_url, + status_code, + ) + self._spotlight_script = None + except urllib.error.URLError as err: + sentry_logger.debug( + "Cannot get Spotlight JS to inject. SpotlightMiddleware will not be very useful.", + exc_info=err, + ) + self._spotlight_script = None + + 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 `` 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: + return None + + try: + spotlight = ( + urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") + ) except urllib.error.URLError: return None else: - event_id = sentry_sdk.api.capture_exception(exception) + event_id = self.sentry_sdk.capture_exception(exception) return HttpResponseServerError( spotlight.replace( "", - ( - f'' - ''.format( - event_id=event_id - ) + SPOTLIGHT_ERROR_PAGE_SNIPPET.format( + spotlight_url=self._spotlight_url, event_id=event_id ), ) ) @@ -119,6 +217,7 @@ def setup_spotlight(options): 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")) ): with capture_internal_exceptions(): middleware = settings.MIDDLEWARE From 17b87bed6e5bdee9635c8b4490b340af90ffe8d6 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 8 Nov 2024 13:02:54 +0000 Subject: [PATCH 02/10] fix logging --- sentry_sdk/spotlight.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index 98298a9fa1..5b00af6e88 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -5,6 +5,7 @@ import urllib.request import urllib.error import urllib3 +import sys from itertools import chain, product @@ -24,6 +25,9 @@ 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" @@ -112,21 +116,14 @@ def __init__(self, get_response): spotlight_js_url, method="HEAD", ) - status_code = urllib.request.urlopen(req).status - if status_code >= 200 and status_code < 400: - self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( - spotlight_js_url - ) - else: - sentry_logger.debug( - "Could not get Spotlight JS from %s (status: %s), SpotlightMiddleware will not be useful.", - spotlight_js_url, - status_code, - ) - self._spotlight_script = None + urllib.request.urlopen(req) + self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( + spotlight_js_url + ) except urllib.error.URLError as err: sentry_logger.debug( - "Cannot get Spotlight JS to inject. SpotlightMiddleware will not be very useful.", + "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", + spotlight_js_url, exc_info=err, ) self._spotlight_script = None @@ -204,6 +201,10 @@ def process_exception(self, _request, exception): 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") @@ -225,9 +226,9 @@ def setup_spotlight(options): settings.MIDDLEWARE = type(middleware)( chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) ) - logging.info("Enabled Spotlight integration for Django") + logger.info("Enabled Spotlight integration for Django") client = SpotlightClient(url) - logging.info("Enabled Spotlight at %s", url) + logger.info("Enabled Spotlight using sidecar at %s", url) return client From 007e770f0d8be4d898903aae255c471ac32a5098 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 8 Nov 2024 13:08:31 +0000 Subject: [PATCH 03/10] allow lazy init, fix tests --- sentry_sdk/spotlight.py | 53 ++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index 5b00af6e88..079e4f3741 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -86,7 +86,7 @@ def capture_envelope(self, envelope): ] class SpotlightMiddleware(MiddlewareMixin): - _spotlight_script: Optional[str] + __spotlight_script: Optional[str] = None _spotlight_url: str def __init__(self, get_response): @@ -104,29 +104,32 @@ def __init__(self, get_response): ) return None # Spotlight URL has a trailing `/stream` part at the end so split it off - spotlight_url = self._spotlight_url = urllib.parse.urljoin( - spotlight_client.url, "../" - ) + self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") + + @property + def spotlight_script(self): + # type: (Self) -> Optional[str] + if 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_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, + ) - try: - spotlight_js_url = urllib.parse.urljoin( - 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_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, - ) - self._spotlight_script = None + return self.__spotlight_script def process_response(self, _request, response): # type: (Self, HttpRequest, HttpResponse) -> Optional[HttpResponse] @@ -143,12 +146,12 @@ def process_response(self, _request, response): encoding = "utf-8" if ( - self._spotlight_script is not None + 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 = self.spotlight_script.encode(encoding) injection_site = next( ( idx From 5741196096adb79bf1e8a5780b2448efbc3066aa Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 8 Nov 2024 13:10:25 +0000 Subject: [PATCH 04/10] fix lint --- sentry_sdk/spotlight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index 079e4f3741..a3060fd589 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -81,8 +81,8 @@ def capture_envelope(self, envelope): CHARSET_PREFIX = "charset=" BODY_CLOSE_TAG = "" BODY_CLOSE_TAG_POSSIBILITIES = [ - "".join(l) - for l in product(*zip(BODY_CLOSE_TAG.upper(), BODY_CLOSE_TAG.lower())) + "".join(chars) + for chars in product(*zip(BODY_CLOSE_TAG.upper(), BODY_CLOSE_TAG.lower())) ] class SpotlightMiddleware(MiddlewareMixin): From edb47536e1b2d16f77575b77bd1ebd0f09632bcb Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 8 Nov 2024 13:11:06 +0000 Subject: [PATCH 05/10] fix moar lint --- sentry_sdk/spotlight.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index a3060fd589..7f180931a2 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -16,6 +16,7 @@ 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, @@ -64,8 +65,6 @@ def capture_envelope(self, envelope): try: - from typing import Self, Optional - from django.utils.deprecation import MiddlewareMixin from django.http import HttpResponseServerError, HttpResponse, HttpRequest from django.conf import settings From 798256dc1903eecd3344d7eebfc993bdb2f03231 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 8 Nov 2024 13:15:28 +0000 Subject: [PATCH 06/10] silence bogus type error --- sentry_sdk/spotlight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index 7f180931a2..70dee31ee3 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -84,7 +84,7 @@ def capture_envelope(self, envelope): for chars in product(*zip(BODY_CLOSE_TAG.upper(), BODY_CLOSE_TAG.lower())) ] - class SpotlightMiddleware(MiddlewareMixin): + class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] __spotlight_script: Optional[str] = None _spotlight_url: str From ad7017962dd99975dfa8561990662d15aea362d1 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 8 Nov 2024 13:18:10 +0000 Subject: [PATCH 07/10] antics of typing --- sentry_sdk/spotlight.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index 70dee31ee3..ca23fa643d 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -85,8 +85,7 @@ def capture_envelope(self, envelope): ] class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] - __spotlight_script: Optional[str] = None - _spotlight_url: str + __spotlight_script = None # type: Optional[str] def __init__(self, get_response): # type: (Self, Callable[..., HttpResponse]) -> None From 66520521cecd4009ed7c42e2e9a4818b5893b616 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 11 Nov 2024 21:46:24 +0000 Subject: [PATCH 08/10] Use `self.sentry_sdk` Co-authored-by: Ivana Kellyer --- sentry_sdk/spotlight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index ca23fa643d..ea3df5605f 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -95,7 +95,7 @@ def __init__(self, get_response): self.sentry_sdk = sentry_sdk.api - spotlight_client = sentry_sdk.api.get_client().spotlight + 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." From 677bca5b43f7623b7d82e5d73f70caa71e3adc9f Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 11 Nov 2024 21:52:51 +0000 Subject: [PATCH 09/10] reduce possibilities --- sentry_sdk/spotlight.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index ea3df5605f..2000d5fc21 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -78,11 +78,11 @@ def capture_envelope(self, envelope): '\n' ) CHARSET_PREFIX = "charset=" - BODY_CLOSE_TAG = "" - BODY_CLOSE_TAG_POSSIBILITIES = [ - "".join(chars) - for chars in product(*zip(BODY_CLOSE_TAG.upper(), BODY_CLOSE_TAG.lower())) - ] + 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] From 15cc2cc90aa24b2dc89c7f16c9a8f4f4ab964ff8 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 12 Nov 2024 13:14:15 +0000 Subject: [PATCH 10/10] dunder mifflin' --- sentry_sdk/spotlight.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index 2000d5fc21..806ba5a09e 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -85,7 +85,7 @@ def capture_envelope(self, envelope): ) class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] - __spotlight_script = None # type: Optional[str] + _spotlight_script = None # type: Optional[str] def __init__(self, get_response): # type: (Self, Callable[..., HttpResponse]) -> None @@ -107,7 +107,7 @@ def __init__(self, get_response): @property def spotlight_script(self): # type: (Self) -> Optional[str] - if self.__spotlight_script is None: + if self._spotlight_script is None: try: spotlight_js_url = urllib.parse.urljoin( self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH @@ -117,7 +117,7 @@ def spotlight_script(self): method="HEAD", ) urllib.request.urlopen(req) - self.__spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( + self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( spotlight_js_url ) except urllib.error.URLError as err: @@ -127,7 +127,7 @@ def spotlight_script(self): exc_info=err, ) - return self.__spotlight_script + return self._spotlight_script def process_response(self, _request, response): # type: (Self, HttpRequest, HttpResponse) -> Optional[HttpResponse]