Repository: getsentry/sentry-python

Issue:
Checkins are dropped when lots of Celery child tasks are started and `max-tasks-per-child` is reached
### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

1.33.0

### Steps to Reproduce

Set `max-tasks-per-child` in Celery to 10000. Use `redis` as broker. Create a Celery Task that starts 12000 child tasks using `.delay()`. 
When `max-tasks-per-child` is reached our redis integration adds a lot of big breadcrumbs to the scope which are applied to all the success/failure checkins of the child spans. 
Because of the size of the breadcrumbs the checkin envelopes are too big and are dropped by the server. Leading to all child spans timing out because the success/failure checking never reaches sentry.

### Expected Result

All checkins are sent successfully

### Actual Result

The checkins are dropped because of size restriction on the server.

Discussion:

Patch:
diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py
index bfe4b4ab..c421a675 100644
--- a/sentry_sdk/_types.py
+++ b/sentry_sdk/_types.py
@@ -54,6 +54,7 @@ if TYPE_CHECKING:
         "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 a3e4b5a9..de4f9977 100644
--- a/sentry_sdk/envelope.py
+++ b/sentry_sdk/envelope.py
@@ -262,6 +262,8 @@ class Item(object):
             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 7f1a79ab..b5eeb0be 100644
--- a/sentry_sdk/integrations/rq.py
+++ b/sentry_sdk/integrations/rq.py
@@ -99,7 +99,7 @@ class RqIntegration(Integration):
             # 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 d2768fb3..b9071cc6 100644
--- a/sentry_sdk/scope.py
+++ b/sentry_sdk/scope.py
@@ -560,69 +560,62 @@ class Scope(object):
 
         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 @@ class Scope(object):
                 "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 4b12287e..8eb00bed 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


Test patch:
diff --git a/tests/test_crons.py b/tests/test_crons.py
index 9ea98df2..39d02a5d 100644
--- a/tests/test_crons.py
+++ b/tests/test_crons.py
@@ -4,6 +4,8 @@ import uuid
 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
+        )