Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(django): Add SpotlightMiddleware when Spotlight is enabled #3600

Merged
merged 7 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from sentry_sdk.metrics import MetricsAggregator
from sentry_sdk.scope import Scope
from sentry_sdk.session import Session
from sentry_sdk.spotlight import SpotlightClient
from sentry_sdk.transport import Transport

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

spotlight = None # type: Optional[SpotlightClient]

def __init__(self, options=None):
# type: (Optional[Dict[str, Any]]) -> None
self.options = (
Expand Down Expand Up @@ -385,7 +388,6 @@ def _capture_envelope(envelope):
disabled_integrations=self.options["disabled_integrations"],
)

self.spotlight = None
spotlight_config = self.options.get("spotlight")
if spotlight_config is None and "SENTRY_SPOTLIGHT" in os.environ:
spotlight_env_value = os.environ["SENTRY_SPOTLIGHT"]
Expand Down
53 changes: 52 additions & 1 deletion sentry_sdk/spotlight.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import io
import os
import urllib.parse
import urllib.request
import urllib.error
import urllib3

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 sentry_sdk.utils import logger
from sentry_sdk.utils import logger, env_to_bool
from sentry_sdk.envelope import Envelope


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


try:
from django.http import HttpResponseServerError
from django.conf import settings

class SpotlightMiddleware:
def __init__(self, get_response):
# type: (Any, Callable[..., Any]) -> None
self.get_response = get_response

def __call__(self, request):
# type: (Any, Any) -> Any
return self.get_response(request)

def process_exception(self, _request, exception):
# type: (Any, Any, Exception) -> Optional[HttpResponseServerError]
if not settings.DEBUG:
return None

import sentry_sdk.api

spotlight_client = sentry_sdk.api.get_client().spotlight
if spotlight_client is None:
return None

# Spotlight URL has a trailing `/stream` part at the end so split it off
spotlight_url = spotlight_client.url.rsplit("/", 1)[0]

try:
spotlight = (
urllib.request.urlopen(spotlight_url).read().decode("utf-8")
).replace("<html>", f'<html><base href="{spotlight_url}">')
except urllib.error.URLError:
return None
else:
sentry_sdk.api.capture_exception(exception)
return HttpResponseServerError(spotlight)

except ImportError:
settings = None


def setup_spotlight(options):
# type: (Dict[str, Any]) -> Optional[SpotlightClient]

Expand All @@ -58,4 +104,9 @@ def setup_spotlight(options):
else:
return None

if settings is not None and env_to_bool(
os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")
):
settings.MIDDLEWARE.append("sentry_sdk.spotlight.SpotlightMiddleware")

return SpotlightClient(url)
32 changes: 32 additions & 0 deletions tests/integrations/django/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1240,3 +1240,35 @@ def test_transaction_http_method_custom(sentry_init, client, capture_events):
(event1, event2) = events
assert event1["request"]["method"] == "OPTIONS"
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.
"""
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.
"""
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
Loading