From 2f53bdea3c66c434c53b6d25a2092f919cb52044 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 31 Jul 2026 08:40:23 +0200 Subject: [PATCH] ref: Remove Spotlight Django integration --- MIGRATION_GUIDE.md | 1 + sentry_sdk/spotlight.py | 161 +----------------------- tests/integrations/django/test_basic.py | 55 -------- 3 files changed, 2 insertions(+), 215 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 5921ff2992..3c297ea9fd 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -37,6 +37,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - A number of extras (installable via `sentry-sdk[extra-name]`) has been removed. Use the base package (`sentry-sdk`) instead; there is no difference in functionality. The following extras have been removed: `aiohttp`, `anthropic`, `arq`, `asyncpg`, `beam`, `bottle`, `celery`, `celery-redbeat`, `chalice`, `clickhouse-driver`, `django`, `falcon`, `fastapi`, `google-genai`, `httpx`, `huey`, `huggingface_hub`, `langchain`, `langgraph`, `launchdarkly`, `litellm`, `litestar`, `loguru`, `mcp`, `openai`, `openfeature`, `pydantic_ai`, `pymongo`, `pyspark`, `rq`, `sanic`, `sqlalchemy`, `starlette`, `starlite`, `statsig`, `tornado`, `unleash`. - The `failed_request_status_codes` integration option now only supports a set of integers as input. Lists of integers or containers of integers are no longer supported. - The deprecated `propagate_traces` option has been removed. Use `trace_propagation_targets` instead, which gives you more power over trace propagation. Note that only the top-level `init` option was removed; the `propagate_traces` option of the Celery integration remains available. +- Removed Spotlight integration for Django. See [Spotlight 2.0](https://github.com/getsentry/spotlight/issues/891) for more context. ## Deprecated diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py index 2dcc86bc47..41877346bc 100644 --- a/sentry_sdk/spotlight.py +++ b/sentry_sdk/spotlight.py @@ -3,20 +3,15 @@ import os import sys import time -import urllib.error -import urllib.parse -import urllib.request -from itertools import chain, product from typing import TYPE_CHECKING import urllib3 if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Self + from typing import Any, Dict, Optional from sentry_sdk.envelope import Envelope from sentry_sdk.utils import ( - capture_internal_exceptions, env_to_bool, ) from sentry_sdk.utils import ( @@ -27,7 +22,6 @@ DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" -DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" class SpotlightClient: @@ -90,145 +84,6 @@ def capture_envelope(self, envelope: "Envelope") -> None: ) -try: - from django.conf import settings - from django.http import HttpRequest, HttpResponse, HttpResponseServerError - from django.utils.deprecation import MiddlewareMixin - - SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" - SPOTLIGHT_JS_SNIPPET_PATTERN = ( - "\n" - '\n' - ) - SPOTLIGHT_ERROR_PAGE_SNIPPET = ( - '\n' - '\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: "Optional[str]" = None - _spotlight_url: "Optional[str]" = None - - def __init__(self: "Self", get_response: "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: "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: "Self", _request: "HttpRequest", response: "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: "Self", _request: "HttpRequest", exception: 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( - "", - SPOTLIGHT_ERROR_PAGE_SNIPPET.format( - spotlight_url=self._spotlight_url, event_id=event_id - ), - ) - ) - -except ImportError: - settings = None - - def _resolve_spotlight_url( spotlight_config: "Any", sentry_logger: "Any" ) -> "Optional[str]": @@ -307,20 +162,6 @@ def setup_spotlight(options: "Dict[str, Any]") -> "Optional[SpotlightClient]": # Update options with resolved URL for consistency options["spotlight"] = url - 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) diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index 886477cf5c..3ab1734f1d 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -2386,61 +2386,6 @@ def test_transaction_http_method_custom( assert event2["request"]["method"] == "HEAD" -def test_ensures_spotlight_middleware_when_spotlight_is_enabled(sentry_init, settings): - """ - Test that ensures if Spotlight is enabled, relevant SpotlightMiddleware - is added to middleware list in settings. - """ - settings.DEBUG = True - original_middleware = frozenset(settings.MIDDLEWARE) - - sentry_init(integrations=[DjangoIntegration()], spotlight=True) - - added = frozenset(settings.MIDDLEWARE) ^ original_middleware - - assert "sentry_sdk.spotlight.SpotlightMiddleware" in added - - -def test_ensures_no_spotlight_middleware_when_env_killswitch_is_false( - monkeypatch, sentry_init, settings -): - """ - Test that ensures if Spotlight is enabled, but is set to a falsy value - the relevant SpotlightMiddleware is NOT added to middleware list in settings. - """ - settings.DEBUG = True - monkeypatch.setenv("SENTRY_SPOTLIGHT_ON_ERROR", "no") - - original_middleware = frozenset(settings.MIDDLEWARE) - - sentry_init(integrations=[DjangoIntegration()], spotlight=True) - - added = frozenset(settings.MIDDLEWARE) ^ original_middleware - - assert "sentry_sdk.spotlight.SpotlightMiddleware" not in added - - -def test_ensures_no_spotlight_middleware_when_no_spotlight( - monkeypatch, sentry_init, settings -): - """ - Test that ensures if Spotlight is not enabled - the relevant SpotlightMiddleware is NOT added to middleware list in settings. - """ - settings.DEBUG = True - - # We should NOT have the middleware even if the env var is truthy if Spotlight is off - monkeypatch.setenv("SENTRY_SPOTLIGHT_ON_ERROR", "1") - - original_middleware = frozenset(settings.MIDDLEWARE) - - sentry_init(integrations=[DjangoIntegration()], spotlight=False) - - added = frozenset(settings.MIDDLEWARE) ^ original_middleware - - assert "sentry_sdk.spotlight.SpotlightMiddleware" not in added - - def test_get_frame_name_when_in_lazy_object(): allowed_to_init = False