Skip to content

Commit

Permalink
Add exception handling to Asyncio Integration (#1695)
Browse files Browse the repository at this point in the history
Make sure that we also capture exceptions from spawned async Tasks.

Co-authored-by: Neel Shah <neelshah.sa@gmail.com>
  • Loading branch information
antonpirker and sl0thentr0py committed Oct 20, 2022
1 parent 5aa2436 commit 29431f6
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 1 deletion.
29 changes: 28 additions & 1 deletion sentry_sdk/integrations/asyncio.py
@@ -1,9 +1,12 @@
from __future__ import absolute_import
import sys

from sentry_sdk._compat import reraise
from sentry_sdk.consts import OP
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk._types import MYPY
from sentry_sdk.utils import event_from_exception

try:
import asyncio
Expand All @@ -15,6 +18,8 @@
if MYPY:
from typing import Any

from sentry_sdk._types import ExcInfo


def patch_asyncio():
# type: () -> None
Expand All @@ -31,7 +36,10 @@ async def _coro_creating_hub_and_span():
hub = Hub(Hub.current)
with hub:
with hub.start_span(op=OP.FUNCTION, description=coro.__qualname__):
await coro
try:
await coro
except Exception:
reraise(*_capture_exception(hub))

# Trying to use user set task factory (if there is one)
if orig_task_factory:
Expand All @@ -56,6 +64,25 @@ async def _coro_creating_hub_and_span():
pass


def _capture_exception(hub):
# type: (Hub) -> ExcInfo
exc_info = sys.exc_info()

integration = hub.get_integration(AsyncioIntegration)
if integration is not None:
# If an integration is there, a client has to be there.
client = hub.client # type: Any

event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "asyncio", "handled": False},
)
hub.capture_event(event, hint=hint)

return exc_info


class AsyncioIntegration(Integration):
identifier = "asyncio"

Expand Down
39 changes: 39 additions & 0 deletions tests/integrations/asyncio/test_asyncio.py
Expand Up @@ -22,6 +22,10 @@ async def bar():
await asyncio.sleep(0.01)


async def boom():
1 / 0


@pytest_asyncio.fixture(scope="session")
def event_loop(request):
"""Create an instance of the default event loop for each test case."""
Expand Down Expand Up @@ -116,3 +120,38 @@ async def test_gather(
transaction_event["spans"][2]["parent_span_id"]
== transaction_event["spans"][0]["span_id"]
)


@minimum_python_36
@pytest.mark.asyncio
async def test_exception(
sentry_init,
capture_events,
event_loop,
):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=True,
debug=True,
integrations=[
AsyncioIntegration(),
],
)

events = capture_events()

with sentry_sdk.start_transaction(name="test_exception"):
with sentry_sdk.start_span(op="root", description="not so important"):
tasks = [event_loop.create_task(boom()), event_loop.create_task(bar())]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)

sentry_sdk.flush()

(error_event, _) = events

assert error_event["transaction"] == "test_exception"
assert error_event["contexts"]["trace"]["op"] == "function"
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
assert error_event["exception"]["values"][0]["value"] == "division by zero"
assert error_event["exception"]["values"][0]["mechanism"]["handled"] is False
assert error_event["exception"]["values"][0]["mechanism"]["type"] == "asyncio"

0 comments on commit 29431f6

Please sign in to comment.