diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index bfe4b4ab2b..c421a6756b 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -54,6 +54,7 @@ "internal", "profile", "statsd", + "check_in", ] SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] EndpointType = Literal["store", "envelope"] diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index a3e4b5a940..de4f99774e 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -262,6 +262,8 @@ def data_category(self): return "profile" elif ty == "statsd": return "statsd" + elif ty == "check_in": + return "check_in" else: return "default" diff --git a/sentry_sdk/integrations/rq.py b/sentry_sdk/integrations/rq.py index 7f1a79abed..b5eeb0be85 100644 --- a/sentry_sdk/integrations/rq.py +++ b/sentry_sdk/integrations/rq.py @@ -99,7 +99,7 @@ def sentry_patched_handle_exception(self, job, *exc_info, **kwargs): # Note, the order of the `or` here is important, # because calling `job.is_failed` will change `_status`. if job._status == JobStatus.FAILED or job.is_failed: - _capture_exception(exc_info) # type: ignore + _capture_exception(exc_info) return old_handle_exception(self, job, *exc_info, **kwargs) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index d2768fb374..b9071cc694 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -560,69 +560,62 @@ def func(event, exc_info): self._error_processors.append(func) - @_disable_capture - def apply_to_event( - self, - event, # type: Event - hint, # type: Hint - options=None, # type: Optional[Dict[str, Any]] - ): - # type: (...) -> Optional[Event] - """Applies the information contained on the scope to the given event.""" - - def _drop(cause, ty): - # type: (Any, str) -> Optional[Any] - logger.info("%s (%s) dropped event", ty, cause) - return None - - is_transaction = event.get("type") == "transaction" - - # put all attachments into the hint. This lets callbacks play around - # with attachments. We also later pull this out of the hint when we - # create the envelope. - attachments_to_send = hint.get("attachments") or [] - for attachment in self._attachments: - if not is_transaction or attachment.add_to_transactions: - attachments_to_send.append(attachment) - hint["attachments"] = attachments_to_send - + def _apply_level_to_event(self, event, hint, options): + # type: (Event, Hint, Optional[Dict[str, Any]]) -> None if self._level is not None: event["level"] = self._level - if not is_transaction: - event.setdefault("breadcrumbs", {}).setdefault("values", []).extend( - self._breadcrumbs - ) + def _apply_breadcrumbs_to_event(self, event, hint, options): + # type: (Event, Hint, Optional[Dict[str, Any]]) -> None + event.setdefault("breadcrumbs", {}).setdefault("values", []).extend( + self._breadcrumbs + ) + def _apply_user_to_event(self, event, hint, options): + # type: (Event, Hint, Optional[Dict[str, Any]]) -> None if event.get("user") is None and self._user is not None: event["user"] = self._user + def _apply_transaction_name_to_event(self, event, hint, options): + # type: (Event, Hint, Optional[Dict[str, Any]]) -> None if event.get("transaction") is None and self._transaction is not None: event["transaction"] = self._transaction + def _apply_transaction_info_to_event(self, event, hint, options): + # type: (Event, Hint, Optional[Dict[str, Any]]) -> None if event.get("transaction_info") is None and self._transaction_info is not None: event["transaction_info"] = self._transaction_info + def _apply_fingerprint_to_event(self, event, hint, options): + # type: (Event, Hint, Optional[Dict[str, Any]]) -> None if event.get("fingerprint") is None and self._fingerprint is not None: event["fingerprint"] = self._fingerprint + def _apply_extra_to_event(self, event, hint, options): + # type: (Event, Hint, Optional[Dict[str, Any]]) -> None if self._extras: event.setdefault("extra", {}).update(self._extras) + def _apply_tags_to_event(self, event, hint, options): + # type: (Event, Hint, Optional[Dict[str, Any]]) -> None if self._tags: event.setdefault("tags", {}).update(self._tags) + def _apply_contexts_to_event(self, event, hint, options): + # type: (Event, Hint, Optional[Dict[str, Any]]) -> None if self._contexts: event.setdefault("contexts", {}).update(self._contexts) contexts = event.setdefault("contexts", {}) + # Add "trace" context if contexts.get("trace") is None: if has_tracing_enabled(options) and self._span is not None: contexts["trace"] = self._span.get_trace_context() else: contexts["trace"] = self.get_trace_context() + # Add "reply_id" context try: replay_id = contexts["trace"]["dynamic_sampling_context"]["replay_id"] except (KeyError, TypeError): @@ -633,14 +626,58 @@ def _drop(cause, ty): "replay_id": replay_id, } + @_disable_capture + def apply_to_event( + self, + event, # type: Event + hint, # type: Hint + options=None, # type: Optional[Dict[str, Any]] + ): + # type: (...) -> Optional[Event] + """Applies the information contained on the scope to the given event.""" + ty = event.get("type") + is_transaction = ty == "transaction" + is_check_in = ty == "check_in" + + # put all attachments into the hint. This lets callbacks play around + # with attachments. We also later pull this out of the hint when we + # create the envelope. + attachments_to_send = hint.get("attachments") or [] + for attachment in self._attachments: + if not is_transaction or attachment.add_to_transactions: + attachments_to_send.append(attachment) + hint["attachments"] = attachments_to_send + + self._apply_contexts_to_event(event, hint, options) + + if not is_check_in: + self._apply_level_to_event(event, hint, options) + self._apply_fingerprint_to_event(event, hint, options) + self._apply_user_to_event(event, hint, options) + self._apply_transaction_name_to_event(event, hint, options) + self._apply_transaction_info_to_event(event, hint, options) + self._apply_tags_to_event(event, hint, options) + self._apply_extra_to_event(event, hint, options) + + if not is_transaction and not is_check_in: + self._apply_breadcrumbs_to_event(event, hint, options) + + def _drop(cause, ty): + # type: (Any, str) -> Optional[Any] + logger.info("%s (%s) dropped event", ty, cause) + return None + + # run error processors exc_info = hint.get("exc_info") if exc_info is not None: for error_processor in self._error_processors: new_event = error_processor(event, exc_info) if new_event is None: return _drop(error_processor, "error processor") + event = new_event + # run event processors for event_processor in chain(global_event_processors, self._event_processors): new_event = event with capture_internal_exceptions(): diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 4b12287ec9..8eb00bed12 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -586,7 +586,7 @@ def make_transport(options): elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): transport_cls = ref_transport elif callable(ref_transport): - return _FunctionTransport(ref_transport) # type: ignore + return _FunctionTransport(ref_transport) # if a transport class is given only instantiate it if the dsn is not # empty or None diff --git a/tests/test_crons.py b/tests/test_crons.py index 9ea98df2ac..39d02a5d47 100644 --- a/tests/test_crons.py +++ b/tests/test_crons.py @@ -4,6 +4,8 @@ import sentry_sdk from sentry_sdk.crons import capture_checkin +from sentry_sdk import Hub, configure_scope, set_level + try: from unittest import mock # python 3.3 and above except ImportError: @@ -220,3 +222,62 @@ def test_capture_checkin_sdk_not_initialized(): duration=None, ) assert check_in_id == "112233" + + +def test_scope_data_in_checkin(sentry_init, capture_envelopes): + sentry_init() + envelopes = capture_envelopes() + + valid_keys = [ + # Mandatory event keys + "type", + "event_id", + "timestamp", + "platform", + # Optional event keys + "release", + "environment", + # Mandatory check-in specific keys + "check_in_id", + "monitor_slug", + "status", + # Optional check-in specific keys + "duration", + "monitor_config", + "contexts", # an event processor adds this + # TODO: These fields need to be checked if valid for checkin: + "_meta", + "tags", + "extra", # an event processor adds this + "modules", + "server_name", + "sdk", + ] + + hub = Hub.current + with configure_scope() as scope: + # Add some data to the scope + set_level("warning") + hub.add_breadcrumb(message="test breadcrumb") + scope.set_tag("test_tag", "test_value") + scope.set_extra("test_extra", "test_value") + scope.set_context("test_context", {"test_key": "test_value"}) + + capture_checkin( + monitor_slug="abc123", + check_in_id="112233", + status="ok", + duration=123, + ) + + (envelope,) = envelopes + check_in_event = envelope.items[0].payload.json + + invalid_keys = [] + for key in check_in_event.keys(): + if key not in valid_keys: + invalid_keys.append(key) + + assert len(invalid_keys) == 0, "Unexpected keys found in checkin: {}".format( + invalid_keys + )