Skip to content

Commit 29431f6

Browse files
Add exception handling to Asyncio Integration (#1695)
Make sure that we also capture exceptions from spawned async Tasks. Co-authored-by: Neel Shah <neelshah.sa@gmail.com>
1 parent 5aa2436 commit 29431f6

File tree

2 files changed

+67
-1
lines changed

2 files changed

+67
-1
lines changed

sentry_sdk/integrations/asyncio.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from __future__ import absolute_import
2+
import sys
23

4+
from sentry_sdk._compat import reraise
35
from sentry_sdk.consts import OP
46
from sentry_sdk.hub import Hub
57
from sentry_sdk.integrations import Integration, DidNotEnable
68
from sentry_sdk._types import MYPY
9+
from sentry_sdk.utils import event_from_exception
710

811
try:
912
import asyncio
@@ -15,6 +18,8 @@
1518
if MYPY:
1619
from typing import Any
1720

21+
from sentry_sdk._types import ExcInfo
22+
1823

1924
def patch_asyncio():
2025
# type: () -> None
@@ -31,7 +36,10 @@ async def _coro_creating_hub_and_span():
3136
hub = Hub(Hub.current)
3237
with hub:
3338
with hub.start_span(op=OP.FUNCTION, description=coro.__qualname__):
34-
await coro
39+
try:
40+
await coro
41+
except Exception:
42+
reraise(*_capture_exception(hub))
3543

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

5866

67+
def _capture_exception(hub):
68+
# type: (Hub) -> ExcInfo
69+
exc_info = sys.exc_info()
70+
71+
integration = hub.get_integration(AsyncioIntegration)
72+
if integration is not None:
73+
# If an integration is there, a client has to be there.
74+
client = hub.client # type: Any
75+
76+
event, hint = event_from_exception(
77+
exc_info,
78+
client_options=client.options,
79+
mechanism={"type": "asyncio", "handled": False},
80+
)
81+
hub.capture_event(event, hint=hint)
82+
83+
return exc_info
84+
85+
5986
class AsyncioIntegration(Integration):
6087
identifier = "asyncio"
6188

tests/integrations/asyncio/test_asyncio.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ async def bar():
2222
await asyncio.sleep(0.01)
2323

2424

25+
async def boom():
26+
1 / 0
27+
28+
2529
@pytest_asyncio.fixture(scope="session")
2630
def event_loop(request):
2731
"""Create an instance of the default event loop for each test case."""
@@ -116,3 +120,38 @@ async def test_gather(
116120
transaction_event["spans"][2]["parent_span_id"]
117121
== transaction_event["spans"][0]["span_id"]
118122
)
123+
124+
125+
@minimum_python_36
126+
@pytest.mark.asyncio
127+
async def test_exception(
128+
sentry_init,
129+
capture_events,
130+
event_loop,
131+
):
132+
sentry_init(
133+
traces_sample_rate=1.0,
134+
send_default_pii=True,
135+
debug=True,
136+
integrations=[
137+
AsyncioIntegration(),
138+
],
139+
)
140+
141+
events = capture_events()
142+
143+
with sentry_sdk.start_transaction(name="test_exception"):
144+
with sentry_sdk.start_span(op="root", description="not so important"):
145+
tasks = [event_loop.create_task(boom()), event_loop.create_task(bar())]
146+
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
147+
148+
sentry_sdk.flush()
149+
150+
(error_event, _) = events
151+
152+
assert error_event["transaction"] == "test_exception"
153+
assert error_event["contexts"]["trace"]["op"] == "function"
154+
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
155+
assert error_event["exception"]["values"][0]["value"] == "division by zero"
156+
assert error_event["exception"]["values"][0]["mechanism"]["handled"] is False
157+
assert error_event["exception"]["values"][0]["mechanism"]["type"] == "asyncio"

0 commit comments

Comments
 (0)