Skip to content

Commit 00f8140

Browse files
authored
feat(django): Add SpotlightMiddleware when Spotlight is enabled (#3600)
This patch replaces Django's debug error page with Spotlight when it is enabled and is running. It bails when DEBUG is False, when it cannot connect to the Spotlight web server, or when explicitly turned off with SENTRY_SPOTLIGHT_ON_ERROR=0.
1 parent 2bfce50 commit 00f8140

File tree

3 files changed

+106
-2
lines changed

3 files changed

+106
-2
lines changed

sentry_sdk/client.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from sentry_sdk.metrics import MetricsAggregator
6262
from sentry_sdk.scope import Scope
6363
from sentry_sdk.session import Session
64+
from sentry_sdk.spotlight import SpotlightClient
6465
from sentry_sdk.transport import Transport
6566

6667
I = TypeVar("I", bound=Integration) # noqa: E741
@@ -153,6 +154,8 @@ class BaseClient:
153154
The basic definition of a client that is used for sending data to Sentry.
154155
"""
155156

157+
spotlight = None # type: Optional[SpotlightClient]
158+
156159
def __init__(self, options=None):
157160
# type: (Optional[Dict[str, Any]]) -> None
158161
self.options = (
@@ -385,7 +388,6 @@ def _capture_envelope(envelope):
385388
disabled_integrations=self.options["disabled_integrations"],
386389
)
387390

388-
self.spotlight = None
389391
spotlight_config = self.options.get("spotlight")
390392
if spotlight_config is None and "SENTRY_SPOTLIGHT" in os.environ:
391393
spotlight_env_value = os.environ["SENTRY_SPOTLIGHT"]

sentry_sdk/spotlight.py

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import io
2+
import os
3+
import urllib.parse
4+
import urllib.request
5+
import urllib.error
26
import urllib3
37

48
from typing import TYPE_CHECKING
59

610
if TYPE_CHECKING:
711
from typing import Any
12+
from typing import Callable
813
from typing import Dict
914
from typing import Optional
1015

11-
from sentry_sdk.utils import logger
16+
from sentry_sdk.utils import logger, env_to_bool
1217
from sentry_sdk.envelope import Envelope
1318

1419

@@ -46,6 +51,47 @@ def capture_envelope(self, envelope):
4651
logger.warning(str(e))
4752

4853

54+
try:
55+
from django.http import HttpResponseServerError
56+
from django.conf import settings
57+
58+
class SpotlightMiddleware:
59+
def __init__(self, get_response):
60+
# type: (Any, Callable[..., Any]) -> None
61+
self.get_response = get_response
62+
63+
def __call__(self, request):
64+
# type: (Any, Any) -> Any
65+
return self.get_response(request)
66+
67+
def process_exception(self, _request, exception):
68+
# type: (Any, Any, Exception) -> Optional[HttpResponseServerError]
69+
if not settings.DEBUG:
70+
return None
71+
72+
import sentry_sdk.api
73+
74+
spotlight_client = sentry_sdk.api.get_client().spotlight
75+
if spotlight_client is None:
76+
return None
77+
78+
# Spotlight URL has a trailing `/stream` part at the end so split it off
79+
spotlight_url = spotlight_client.url.rsplit("/", 1)[0]
80+
81+
try:
82+
spotlight = (
83+
urllib.request.urlopen(spotlight_url).read().decode("utf-8")
84+
).replace("<html>", f'<html><base href="{spotlight_url}">')
85+
except urllib.error.URLError:
86+
return None
87+
else:
88+
sentry_sdk.api.capture_exception(exception)
89+
return HttpResponseServerError(spotlight)
90+
91+
except ImportError:
92+
settings = None
93+
94+
4995
def setup_spotlight(options):
5096
# type: (Dict[str, Any]) -> Optional[SpotlightClient]
5197

@@ -58,4 +104,9 @@ def setup_spotlight(options):
58104
else:
59105
return None
60106

107+
if settings is not None and env_to_bool(
108+
os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")
109+
):
110+
settings.MIDDLEWARE.append("sentry_sdk.spotlight.SpotlightMiddleware")
111+
61112
return SpotlightClient(url)

tests/integrations/django/test_basic.py

+51
Original file line numberDiff line numberDiff line change
@@ -1240,3 +1240,54 @@ def test_transaction_http_method_custom(sentry_init, client, capture_events):
12401240
(event1, event2) = events
12411241
assert event1["request"]["method"] == "OPTIONS"
12421242
assert event2["request"]["method"] == "HEAD"
1243+
1244+
1245+
def test_ensures_spotlight_middleware_when_spotlight_is_enabled(sentry_init, settings):
1246+
"""
1247+
Test that ensures if Spotlight is enabled, relevant SpotlightMiddleware
1248+
is added to middleware list in settings.
1249+
"""
1250+
original_middleware = frozenset(settings.MIDDLEWARE)
1251+
1252+
sentry_init(integrations=[DjangoIntegration()], spotlight=True)
1253+
1254+
added = frozenset(settings.MIDDLEWARE) ^ original_middleware
1255+
1256+
assert "sentry_sdk.spotlight.SpotlightMiddleware" in added
1257+
1258+
1259+
def test_ensures_no_spotlight_middleware_when_env_killswitch_is_false(
1260+
monkeypatch, sentry_init, settings
1261+
):
1262+
"""
1263+
Test that ensures if Spotlight is enabled, but is set to a falsy value
1264+
the relevant SpotlightMiddleware is NOT added to middleware list in settings.
1265+
"""
1266+
monkeypatch.setenv("SENTRY_SPOTLIGHT_ON_ERROR", "no")
1267+
1268+
original_middleware = frozenset(settings.MIDDLEWARE)
1269+
1270+
sentry_init(integrations=[DjangoIntegration()], spotlight=True)
1271+
1272+
added = frozenset(settings.MIDDLEWARE) ^ original_middleware
1273+
1274+
assert "sentry_sdk.spotlight.SpotlightMiddleware" not in added
1275+
1276+
1277+
def test_ensures_no_spotlight_middleware_when_no_spotlight(
1278+
monkeypatch, sentry_init, settings
1279+
):
1280+
"""
1281+
Test that ensures if Spotlight is not enabled
1282+
the relevant SpotlightMiddleware is NOT added to middleware list in settings.
1283+
"""
1284+
# We should NOT have the middleware even if the env var is truthy if Spotlight is off
1285+
monkeypatch.setenv("SENTRY_SPOTLIGHT_ON_ERROR", "1")
1286+
1287+
original_middleware = frozenset(settings.MIDDLEWARE)
1288+
1289+
sentry_init(integrations=[DjangoIntegration()], spotlight=False)
1290+
1291+
added = frozenset(settings.MIDDLEWARE) ^ original_middleware
1292+
1293+
assert "sentry_sdk.spotlight.SpotlightMiddleware" not in added

0 commit comments

Comments
 (0)