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 `