diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 749ab23cfe..21d5f323c3 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -33,6 +33,7 @@ from sentry_sdk.profiler import has_profiling_enabled, setup_profiler from sentry_sdk.scrubber import EventScrubber from sentry_sdk.monitor import Monitor +from sentry_sdk.spotlight import setup_spotlight from sentry_sdk._types import TYPE_CHECKING @@ -268,6 +269,10 @@ def _capture_envelope(envelope): ], ) + self.spotlight = None + if self.options.get("spotlight"): + self.spotlight = setup_spotlight(self.options) + sdk_name = get_sdk_name(list(self.integrations.keys())) SDK_INFO["name"] = sdk_name logger.debug("Setting SDK name to '%s'", sdk_name) @@ -548,8 +553,6 @@ def capture_event( if disable_capture_event.get(False): return None - if self.transport is None: - return None if hint is None: hint = {} event_id = event.get("event_id") @@ -591,7 +594,11 @@ def capture_event( # If tracing is enabled all events should go to /envelope endpoint. # If no tracing is enabled only transactions, events with attachments, and checkins should go to the /envelope endpoint. should_use_envelope_endpoint = ( - tracing_enabled or is_transaction or is_checkin or bool(attachments) + tracing_enabled + or is_transaction + or is_checkin + or bool(attachments) + or bool(self.spotlight) ) if should_use_envelope_endpoint: headers = { @@ -616,9 +623,18 @@ def capture_event( for attachment in attachments or (): envelope.add_item(attachment.to_envelope_item()) + if self.spotlight: + self.spotlight.capture_envelope(envelope) + + if self.transport is None: + return None + self.transport.capture_envelope(envelope) else: + if self.transport is None: + return None + # All other events go to the legacy /store/ endpoint (will be removed in the future). self.transport.capture_event(event_opt) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index f51ba52afc..b69a4de21b 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -263,6 +263,7 @@ def __init__( max_value_length=DEFAULT_MAX_VALUE_LENGTH, # type: int enable_backpressure_handling=True, # type: bool error_sampler=None, # type: Optional[Callable[[Event, Hint], Union[float, bool]]] + spotlight=None, # type: Optional[Union[bool, str]] ): # type: (...) -> None pass diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py new file mode 100644 index 0000000000..9b686bfc89 --- /dev/null +++ b/sentry_sdk/spotlight.py @@ -0,0 +1,51 @@ +import io +import urllib3 + +from sentry_sdk._types import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + from typing import Dict + from typing import Optional + +from sentry_sdk.utils import logger +from sentry_sdk.envelope import Envelope + + +class SpotlightClient(object): + def __init__(self, url): + # type: (str) -> None + self.url = url + self.http = urllib3.PoolManager() + + def capture_envelope(self, envelope): + # type: (Envelope) -> None + body = io.BytesIO() + envelope.serialize_into(body) + try: + req = self.http.request( + url=self.url, + body=body.getvalue(), + method="POST", + headers={ + "Content-Type": "application/x-sentry-envelope", + }, + ) + req.close() + except Exception as e: + logger.exception(str(e)) + + +def setup_spotlight(options): + # type: (Dict[str, Any]) -> Optional[SpotlightClient] + + url = options.get("spotlight") + + if isinstance(url, str): + pass + elif url is True: + url = "http://localhost:8969/stream" + else: + return None + + return SpotlightClient(url) diff --git a/tests/test_spotlight.py b/tests/test_spotlight.py new file mode 100644 index 0000000000..f0ab4664e0 --- /dev/null +++ b/tests/test_spotlight.py @@ -0,0 +1,56 @@ +import pytest + +from sentry_sdk import Hub, capture_exception + + +@pytest.fixture +def capture_spotlight_envelopes(monkeypatch): + def inner(): + envelopes = [] + test_spotlight = Hub.current.client.spotlight + old_capture_envelope = test_spotlight.capture_envelope + + def append_envelope(envelope): + envelopes.append(envelope) + return old_capture_envelope(envelope) + + monkeypatch.setattr(test_spotlight, "capture_envelope", append_envelope) + return envelopes + + return inner + + +def test_spotlight_off_by_default(sentry_init): + sentry_init() + assert Hub.current.client.spotlight is None + + +def test_spotlight_default_url(sentry_init): + sentry_init(spotlight=True) + + spotlight = Hub.current.client.spotlight + assert spotlight is not None + assert spotlight.url == "http://localhost:8969/stream" + + +def test_spotlight_custom_url(sentry_init): + sentry_init(spotlight="http://foobar@test.com/132") + + spotlight = Hub.current.client.spotlight + assert spotlight is not None + assert spotlight.url == "http://foobar@test.com/132" + + +def test_spotlight_envelope(sentry_init, capture_spotlight_envelopes): + sentry_init(spotlight=True) + envelopes = capture_spotlight_envelopes() + + try: + raise ValueError("aha!") + except Exception: + capture_exception() + + (envelope,) = envelopes + payload = envelope.items[0].payload.json + + assert payload["exception"]["values"][0]["value"] == "aha!"