Repository: getsentry/sentry-python

Issue:
asyncio concurrency issues
@mitsuhiko  discovered that passing around hubs does not work at all and we likely leak state between requests


Discussion:

Resolving patch:
diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py
--- a/sentry_sdk/integrations/sanic.py
+++ b/sentry_sdk/integrations/sanic.py
@@ -36,17 +36,19 @@ def setup_once():
         async def sentry_handle_request(self, request, *args, **kwargs):
             hub = Hub.current
             if hub.get_integration(SanicIntegration) is None:
-                response = old_handle_request(self, request, *args, **kwargs)
-            else:
-                weak_request = weakref.ref(request)
+                return old_handle_request(self, request, *args, **kwargs)
+
+            weak_request = weakref.ref(request)
 
-                with hub.push_scope() as scope:
+            with Hub(hub) as hub:
+                with hub.configure_scope() as scope:
                     scope.add_event_processor(_make_request_processor(weak_request))
-                    response = old_handle_request(self, request, *args, **kwargs)
-                    if isawaitable(response):
-                        response = await response
 
-            return response
+                response = old_handle_request(self, request, *args, **kwargs)
+                if isawaitable(response):
+                    response = await response
+
+                return response
 
         Sanic.handle_request = sentry_handle_request
 


Test patch:
diff --git a/tests/integrations/sanic/__init__.py b/tests/integrations/sanic/__init__.py
new file mode 100644
--- /dev/null
+++ b/tests/integrations/sanic/__init__.py
@@ -0,0 +1,3 @@
+import pytest
+
+sanic = pytest.importorskip("sanic")
diff --git a/tests/integrations/sanic/test_sanic.py b/tests/integrations/sanic/test_sanic.py
--- a/tests/integrations/sanic/test_sanic.py
+++ b/tests/integrations/sanic/test_sanic.py
@@ -1,11 +1,12 @@
-import pytest
+import random
+import asyncio
 
-sanic = pytest.importorskip("sanic")
+import pytest
 
-from sentry_sdk import capture_message
+from sentry_sdk import capture_message, configure_scope
 from sentry_sdk.integrations.sanic import SanicIntegration
 
-from sanic import Sanic, response
+from sanic import Sanic, request, response
 
 
 @pytest.fixture
@@ -67,7 +68,7 @@ def myerror(request):
     assert exception["type"] == "ValueError"
     assert exception["value"] == "oh no"
     assert any(
-        frame["filename"] == "test_sanic.py"
+        frame["filename"].endswith("test_sanic.py")
         for frame in exception["stacktrace"]["frames"]
     )
 
@@ -92,13 +93,55 @@ def myhandler(request, exception):
     exception, = event1["exception"]["values"]
     assert exception["type"] == "ValueError"
     assert any(
-        frame["filename"] == "test_sanic.py"
+        frame["filename"].endswith("test_sanic.py")
         for frame in exception["stacktrace"]["frames"]
     )
 
     exception, = event2["exception"]["values"]
     assert exception["type"] == "ZeroDivisionError"
     assert any(
-        frame["filename"] == "test_sanic.py"
+        frame["filename"].endswith("test_sanic.py")
         for frame in exception["stacktrace"]["frames"]
     )
+
+
+def test_concurrency(sentry_init, app):
+    sentry_init(integrations=[SanicIntegration()])
+
+    @app.route("/context-check/<i>")
+    async def context_check(request, i):
+        with configure_scope() as scope:
+            scope.set_tag("i", i)
+
+        await asyncio.sleep(random.random())
+
+        with configure_scope() as scope:
+            assert scope._tags["i"] == i
+
+        return response.text("ok")
+
+    async def task(i):
+        responses = []
+
+        await app.handle_request(
+            request.Request(
+                url_bytes=f"http://localhost/context-check/{i}".encode("ascii"),
+                headers={},
+                version="1.1",
+                method="GET",
+                transport=None,
+            ),
+            write_callback=responses.append,
+            stream_callback=responses.append,
+        )
+
+        r, = responses
+        assert r.status == 200
+
+    async def runner():
+        await asyncio.gather(*(task(i) for i in range(1000)))
+
+    asyncio.run(runner())
+
+    with configure_scope() as scope:
+        assert not scope._tags