diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 5d0afc0869..ae8f49b79b 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -6,7 +6,11 @@ from sentry_sdk.integrations import Integration from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + HAS_REAL_CONTEXTVARS, +) import asyncio from aiohttp.web import Application, HTTPException # type: ignore @@ -27,11 +31,12 @@ class AioHttpIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - if sys.version_info < (3, 7): + if not HAS_REAL_CONTEXTVARS: # We better have contextvars or we're going to leak state between # requests. raise RuntimeError( - "The aiohttp integration for Sentry requires Python 3.7+" + "The aiohttp integration for Sentry requires Python 3.7+ " + " or aiocontextvars package" ) ignore_logger("aiohttp.server") @@ -61,7 +66,10 @@ async def inner(): return response - return await asyncio.create_task(inner()) + # Explicitly wrap in task such that current contextvar context is + # copied. Just doing `return await inner()` will leak scope data + # between requests. + return await asyncio.get_event_loop().create_task(inner()) Application._handle = sentry_app_handle diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index e0f14b5576..79f4ceb4c2 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -4,7 +4,11 @@ from sentry_sdk._compat import urlparse, reraise from sentry_sdk.hub import Hub -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + HAS_REAL_CONTEXTVARS, +) from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers from sentry_sdk.integrations.logging import ignore_logger @@ -34,10 +38,13 @@ class SanicIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - if sys.version_info < (3, 7): - # Sanic is async. We better have contextvars or we're going to leak - # state between requests. - raise RuntimeError("The sanic integration for Sentry requires Python 3.7+") + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise RuntimeError( + "The sanic integration for Sentry requires Python 3.7+ " + " or aiocontextvars package" + ) # Sanic 0.8 and older creates a logger named "root" and puts a # stringified version of every exception in there (without exc_info), diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index bccade6400..d11d79073e 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -868,9 +868,16 @@ def realign_remark(remark): ) +HAS_REAL_CONTEXTVARS = True + try: from contextvars import ContextVar # type: ignore + + if not PY2 and sys.version_info < (3, 7): + import aiocontextvars # type: ignore # noqa except ImportError: + HAS_REAL_CONTEXTVARS = False + from threading import local class ContextVar(object): # type: ignore diff --git a/test-requirements.txt b/test-requirements.txt index 36cacfa846..79872381ae 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,4 +4,4 @@ pytest-xdist==1.23.0 tox==3.7.0 Werkzeug==0.14.1 pytest-localserver==0.4.1 -pytest-cov==2.6.0 +pytest-cov==2.6.0 \ No newline at end of file diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index d357a02d58..4e18cc6400 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -28,7 +28,7 @@ async def hello(request): assert request["env"] == {"REMOTE_ADDR": "127.0.0.1"} assert request["method"] == "GET" assert request["query_string"] == "" - assert request["url"] == f"http://{host}/" + assert request["url"] == "http://{host}/".format(host=host) assert request["headers"] == { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", diff --git a/tests/integrations/sanic/test_sanic.py b/tests/integrations/sanic/test_sanic.py index 5c59410740..beb60d37bd 100644 --- a/tests/integrations/sanic/test_sanic.py +++ b/tests/integrations/sanic/test_sanic.py @@ -1,3 +1,5 @@ +import sys + import random import asyncio @@ -140,7 +142,9 @@ async def task(i): await app.handle_request( request.Request( - url_bytes=f"http://localhost/context-check/{i}".encode("ascii"), + url_bytes="http://localhost/context-check/{i}".format(i=i).encode( + "ascii" + ), headers={}, version="1.1", method="GET", @@ -156,7 +160,12 @@ async def task(i): async def runner(): await asyncio.gather(*(task(i) for i in range(1000))) - asyncio.run(runner()) + if sys.version_info < (3, 7): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(runner()) + else: + asyncio.run(runner()) with configure_scope() as scope: assert not scope._tags diff --git a/tox.ini b/tox.ini index c4646511fc..13e2913382 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ envlist = {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-flask-{1.0,0.11,0.12,dev} - py3.7-sanic-0.8 + {py3.5,py3.6,py3.7}-sanic-0.8 {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-celery-{4.1,4.2} {pypy,py2.7}-celery-3 @@ -62,6 +62,7 @@ deps = flask-dev: git+https://github.com/pallets/flask.git#egg=flask sanic-0.8: sanic>=0.8,<0.9 + {py3.5,py3.6}-sanic-0.8: aiocontextvars==0.2.1 sanic: aiohttp celery-3: Celery>=3.1,<4.0