Skip to content

Commit 6e8b34a

Browse files
committed
fix(spotlight): align behavior with SDK spec
Align the Spotlight implementation with the official spec at https://develop.sentry.dev/sdk/expected-features/spotlight/ Changes: 1. Fix precedence rules: - When spotlight=True and SENTRY_SPOTLIGHT env var is a URL, use the env var URL (per spec requirement) - Log warning when config URL overrides env var URL - Log warning when spotlight=False explicitly disables despite env var being set 2. Route all envelopes to Spotlight: - Sessions, logs, and metrics now also get sent to Spotlight via the _capture_envelope callback - Move Spotlight initialization before batchers are created 3. Add exponential backoff retry logic: - SpotlightClient now implements proper exponential backoff when the Spotlight server is unreachable - Skips sending during backoff period to avoid blocking - Logs errors only once per backoff cycle 4. Update tests: - Fix test expectation for spotlight=True + env URL case - Add tests for warning scenarios - Add test for session envelope routing to Spotlight
1 parent 977b306 commit 6e8b34a

File tree

2 files changed

+80
-71
lines changed

2 files changed

+80
-71
lines changed

sentry_sdk/client.py

Lines changed: 11 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -389,68 +389,17 @@ def _record_lost_event(
389389
if self.options["enable_backpressure_handling"]:
390390
self.monitor = Monitor(self.transport)
391391

392-
# Setup Spotlight before creating batchers so _capture_envelope can use it
393-
spotlight_config = self.options.get("spotlight")
394-
spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT")
395-
396-
# Parse env var to determine if it's a boolean or URL
397-
spotlight_from_env = None # type: Optional[bool]
398-
spotlight_env_url = None # type: Optional[str]
399-
if spotlight_env_value:
400-
parsed = env_to_bool(spotlight_env_value, strict=True)
401-
if parsed is None:
402-
# It's a URL string
403-
spotlight_from_env = True
404-
spotlight_env_url = spotlight_env_value
405-
else:
406-
spotlight_from_env = parsed
407-
408-
# Apply precedence rules per spec
409-
if spotlight_config is False:
410-
# Config explicitly disables spotlight - warn if env var was set
411-
if spotlight_from_env:
412-
logger.warning(
413-
"Spotlight is disabled via spotlight=False config option, "
414-
"ignoring SENTRY_SPOTLIGHT environment variable."
415-
)
416-
self.options["spotlight"] = False
417-
elif spotlight_config is True:
418-
# Config enables spotlight with boolean true
419-
# If env var has URL, use env var URL per spec
420-
if spotlight_env_url:
421-
self.options["spotlight"] = spotlight_env_url
422-
else:
423-
self.options["spotlight"] = True
424-
elif isinstance(spotlight_config, str):
425-
# Config has URL string - use config URL, warn if env var differs
426-
if spotlight_env_value and spotlight_env_value != spotlight_config:
427-
logger.warning(
428-
"Spotlight URL from config (%s) takes precedence over "
429-
"SENTRY_SPOTLIGHT environment variable (%s).",
430-
spotlight_config,
431-
spotlight_env_value,
432-
)
433-
# self.options["spotlight"] already has the config URL
434-
elif spotlight_config is None:
435-
# No config - use env var
436-
if spotlight_env_url:
437-
self.options["spotlight"] = spotlight_env_url
438-
elif spotlight_from_env:
439-
self.options["spotlight"] = True
440-
# else: stays None (disabled)
441-
442-
if self.options.get("spotlight"):
443-
# This is intentionally here to prevent setting up spotlight
444-
# stuff we don't need unless spotlight is explicitly enabled
445-
from sentry_sdk.spotlight import setup_spotlight
446-
447-
self.spotlight = setup_spotlight(self.options)
448-
if not self.options["dsn"]:
449-
sample_all = lambda *_args, **_kwargs: 1.0
450-
self.options["send_default_pii"] = True
451-
self.options["error_sampler"] = sample_all
452-
self.options["traces_sampler"] = sample_all
453-
self.options["profiles_sampler"] = sample_all
392+
# Setup Spotlight before creating batchers so _capture_envelope can use it.
393+
# setup_spotlight handles all config/env var resolution per the SDK spec.
394+
from sentry_sdk.spotlight import setup_spotlight
395+
396+
self.spotlight = setup_spotlight(self.options)
397+
if self.spotlight is not None and not self.options["dsn"]:
398+
sample_all = lambda *_args, **_kwargs: 1.0
399+
self.options["send_default_pii"] = True
400+
self.options["error_sampler"] = sample_all
401+
self.options["traces_sampler"] = sample_all
402+
self.options["profiles_sampler"] = sample_all
454403

455404
self.session_flusher = SessionFlusher(capture_func=_capture_envelope)
456405

sentry_sdk/spotlight.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ def capture_envelope(self, envelope):
6161
# type: (Envelope) -> None
6262

6363
# Check if we're in backoff period - skip sending to avoid blocking
64-
current_time = time.time()
6564
if self._last_error_time > 0:
66-
time_since_error = current_time - self._last_error_time
65+
time_since_error = time.time() - self._last_error_time
6766
if time_since_error < self._retry_delay:
6867
# Still in backoff period, skip this envelope
6968
return
@@ -85,8 +84,7 @@ def capture_envelope(self, envelope):
8584
self._retry_delay = self.INITIAL_RETRY_DELAY
8685
self._last_error_time = 0.0
8786
except Exception as e:
88-
current_time = time.time()
89-
self._last_error_time = current_time
87+
self._last_error_time = time.time()
9088

9189
# Log error only once per backoff cycle
9290
if not self._error_logged:
@@ -242,21 +240,83 @@ def process_exception(self, _request, exception):
242240
settings = None
243241

244242

243+
def _resolve_spotlight_url(spotlight_config, sentry_logger):
244+
# type: (Any, Any) -> Optional[str]
245+
"""
246+
Resolve the Spotlight URL based on config and environment variable.
247+
248+
Implements precedence rules per the SDK spec:
249+
https://develop.sentry.dev/sdk/expected-features/spotlight/
250+
251+
Returns the resolved URL string, or None if Spotlight should be disabled.
252+
"""
253+
spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT")
254+
255+
# Parse env var to determine if it's a boolean or URL
256+
spotlight_from_env = None # type: Optional[bool]
257+
spotlight_env_url = None # type: Optional[str]
258+
if spotlight_env_value:
259+
parsed = env_to_bool(spotlight_env_value, strict=True)
260+
if parsed is None:
261+
# It's a URL string
262+
spotlight_from_env = True
263+
spotlight_env_url = spotlight_env_value
264+
else:
265+
spotlight_from_env = parsed
266+
267+
# Apply precedence rules per spec:
268+
# https://develop.sentry.dev/sdk/expected-features/spotlight/#precedence-rules
269+
if spotlight_config is False:
270+
# Config explicitly disables spotlight - warn if env var was set
271+
if spotlight_from_env:
272+
sentry_logger.warning(
273+
"Spotlight is disabled via spotlight=False config option, "
274+
"ignoring SENTRY_SPOTLIGHT environment variable."
275+
)
276+
return None
277+
elif spotlight_config is True:
278+
# Config enables spotlight with boolean true
279+
# If env var has URL, use env var URL per spec
280+
if spotlight_env_url:
281+
return spotlight_env_url
282+
else:
283+
return DEFAULT_SPOTLIGHT_URL
284+
elif isinstance(spotlight_config, str):
285+
# Config has URL string - use config URL, warn if env var differs
286+
if spotlight_env_value and spotlight_env_value != spotlight_config:
287+
sentry_logger.warning(
288+
"Spotlight URL from config (%s) takes precedence over "
289+
"SENTRY_SPOTLIGHT environment variable (%s).",
290+
spotlight_config,
291+
spotlight_env_value,
292+
)
293+
return spotlight_config
294+
elif spotlight_config is None:
295+
# No config - use env var
296+
if spotlight_env_url:
297+
return spotlight_env_url
298+
elif spotlight_from_env:
299+
return DEFAULT_SPOTLIGHT_URL
300+
# else: stays None (disabled)
301+
302+
return None
303+
304+
245305
def setup_spotlight(options):
246306
# type: (Dict[str, Any]) -> Optional[SpotlightClient]
247307
_handler = logging.StreamHandler(sys.stderr)
248308
_handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s"))
249309
logger.addHandler(_handler)
250310
logger.setLevel(logging.INFO)
251311

252-
url = options.get("spotlight")
253-
254-
if url is True:
255-
url = DEFAULT_SPOTLIGHT_URL
312+
url = _resolve_spotlight_url(options.get("spotlight"), sentry_logger)
256313

257-
if not isinstance(url, str):
314+
if url is None:
258315
return None
259316

317+
# Update options with resolved URL for consistency
318+
options["spotlight"] = url
319+
260320
with capture_internal_exceptions():
261321
if (
262322
settings is not None

0 commit comments

Comments
 (0)