Repository: getsentry/sentry-python

Issue:
Recursive serializer stackoverflow error when serializing an object that logs on iteration
I'm currently having a problem with the SDKs integrated serialisation of local variables for sending with events.
I have in my local variables, an object that is a `Mapping` but that sometimes raises an event when keys are accessed. This is a normal behavior, since keys are lazy loaded and my code handles these correctly. However, when an event occurs and this variable is serialized for sending to sentry, the sdk iterates over all my keys and causes an infinite recursion.

Minimal code te reproduce :
```python
import logging
from collections.abc import Sequence
import sentry_sdk

logger = logging.getLogger(__name__)

class MyGenerator(Sequence):
    def __init__(self):
        self.values = [1, 2, 3]
        Sequence.__init__(self)
    
    def __iter__(self):
        for value in self.values:
            yield value
        logger.error(f"No values left", exc_info=True)

    def __len__(self):
        """List length"""
        return len(self.values)
        
    def __getitem__(self, ii):
        """Get a list item"""
        return self.values[ii]
    

if __name__ == "__main__":
    sentry_sdk.init(
        dsn="[DSN]"
    )

    gen = MyGenerator()

    logger.error("Testing", exc_info=True)
```

The only quick solution I found is to set `with_locals=False` on the sdk but it's a pity to lack all this functionality because of this case.
On the other hand, I'm not sure how the SDK could anticipate such problems, here's what I'm thinking:
* The SDK could check how deep in recursion it is when serializing and stop when it notices it's handling a log that was generated while handling an other log.
* In the same way that exceptions that occur when deserializing trigger `<failed to serialize, use init(debug=True) to see error logs>`, all logs could be disregarded
* In some cases it could be problematic that sentry iterates over those variables altogether (for example, emptying a generator when it should still have values available) and for those maybe a `serialize_ignore_classes=[MyDangerousClass]` on the SDK init could avoid interference with such classes.

Happy to open a PR to follow-up on this when a proper solution is found !


Discussion:

Resolving patch:
diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py
--- a/sentry_sdk/client.py
+++ b/sentry_sdk/client.py
@@ -31,6 +31,7 @@
 
 
 _client_init_debug = ContextVar("client_init_debug")
+_client_in_capture_event = ContextVar("client_in_capture_event")
 
 
 def _get_options(*args, **kwargs):
@@ -240,20 +241,29 @@ def capture_event(
 
         :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help.
         """
-        if self.transport is None:
+        is_recursive = _client_in_capture_event.get(False)
+        if is_recursive:
             return None
-        if hint is None:
-            hint = {}
-        rv = event.get("event_id")
-        if rv is None:
-            event["event_id"] = rv = uuid.uuid4().hex
-        if not self._should_capture(event, hint, scope):
-            return None
-        event = self._prepare_event(event, hint, scope)
-        if event is None:
-            return None
-        self.transport.capture_event(event)
-        return rv
+
+        _client_in_capture_event.set(True)
+
+        try:
+            if self.transport is None:
+                return None
+            if hint is None:
+                hint = {}
+            event_id = event.get("event_id")
+            if event_id is None:
+                event["event_id"] = event_id = uuid.uuid4().hex
+            if not self._should_capture(event, hint, scope):
+                return None
+            event_opt = self._prepare_event(event, hint, scope)
+            if event_opt is None:
+                return None
+            self.transport.capture_event(event_opt)
+            return event_id
+        finally:
+            _client_in_capture_event.set(False)
 
     def close(
         self,


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -560,6 +560,68 @@ def __repr__(self):
     )
 
 
+def test_mapping_sends_exception(sentry_init, capture_events):
+    sentry_init()
+    events = capture_events()
+
+    class C(Mapping):
+        def __iter__(self):
+            try:
+                1 / 0
+            except ZeroDivisionError:
+                capture_exception()
+            yield "hi"
+
+        def __len__(self):
+            """List length"""
+            return 1
+
+        def __getitem__(self, ii):
+            """Get a list item"""
+            if ii == "hi":
+                return "hi"
+
+            raise KeyError()
+
+    try:
+        a = C()  # noqa
+        1 / 0
+    except Exception:
+        capture_exception()
+
+    event, = events
+
+    assert event["exception"]["values"][0]["stacktrace"]["frames"][0]["vars"]["a"] == {
+        "hi": "'hi'"
+    }
+
+
+def test_object_sends_exception(sentry_init, capture_events):
+    sentry_init()
+    events = capture_events()
+
+    class C(object):
+        def __repr__(self):
+            try:
+                1 / 0
+            except ZeroDivisionError:
+                capture_exception()
+            return "hi, i am a repr"
+
+    try:
+        a = C()  # noqa
+        1 / 0
+    except Exception:
+        capture_exception()
+
+    event, = events
+
+    assert (
+        event["exception"]["values"][0]["stacktrace"]["frames"][0]["vars"]["a"]
+        == "hi, i am a repr"
+    )
+
+
 def test_errno_errors(sentry_init, capture_events):
     sentry_init()
     events = capture_events()