Skip to content

Commit a6e3b50

Browse files
feat(integrations): Add unraisable exception integration (#4733)
Adds an uncaught exception integration, enabled by default. The integration forwards the exception to Sentry only if the exception value and stacktrace are set. Closes #374
1 parent 6d6e8a2 commit a6e3b50

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import sys
2+
3+
import sentry_sdk
4+
from sentry_sdk.utils import (
5+
capture_internal_exceptions,
6+
event_from_exception,
7+
)
8+
from sentry_sdk.integrations import Integration
9+
10+
from typing import TYPE_CHECKING
11+
12+
if TYPE_CHECKING:
13+
from typing import Callable
14+
from typing import Any
15+
16+
17+
class UnraisablehookIntegration(Integration):
18+
identifier = "unraisablehook"
19+
20+
@staticmethod
21+
def setup_once():
22+
# type: () -> None
23+
sys.unraisablehook = _make_unraisable(sys.unraisablehook)
24+
25+
26+
def _make_unraisable(old_unraisablehook):
27+
# type: (Callable[[sys.UnraisableHookArgs], Any]) -> Callable[[sys.UnraisableHookArgs], Any]
28+
def sentry_sdk_unraisablehook(unraisable):
29+
# type: (sys.UnraisableHookArgs) -> None
30+
integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration)
31+
32+
# Note: If we replace this with ensure_integration_enabled then
33+
# we break the exceptiongroup backport;
34+
# See: https://github.com/getsentry/sentry-python/issues/3097
35+
if integration is None:
36+
return old_unraisablehook(unraisable)
37+
38+
if unraisable.exc_value and unraisable.exc_traceback:
39+
with capture_internal_exceptions():
40+
event, hint = event_from_exception(
41+
(
42+
unraisable.exc_type,
43+
unraisable.exc_value,
44+
unraisable.exc_traceback,
45+
),
46+
client_options=sentry_sdk.get_client().options,
47+
mechanism={"type": "unraisablehook", "handled": False},
48+
)
49+
sentry_sdk.capture_event(event, hint=hint)
50+
51+
return old_unraisablehook(unraisable)
52+
53+
return sentry_sdk_unraisablehook
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import pytest
2+
import sys
3+
import subprocess
4+
5+
from textwrap import dedent
6+
7+
8+
TEST_PARAMETERS = [
9+
("", "HttpTransport"),
10+
('_experiments={"transport_http2": True}', "Http2Transport"),
11+
]
12+
13+
minimum_python_38 = pytest.mark.skipif(
14+
sys.version_info < (3, 8),
15+
reason="The unraisable exception hook is only available in Python 3.8 and above.",
16+
)
17+
18+
19+
@minimum_python_38
20+
@pytest.mark.parametrize("options, transport", TEST_PARAMETERS)
21+
def test_unraisablehook(tmpdir, options, transport):
22+
app = tmpdir.join("app.py")
23+
app.write(
24+
dedent(
25+
"""
26+
from sentry_sdk import init, transport
27+
from sentry_sdk.integrations.unraisablehook import UnraisablehookIntegration
28+
29+
class Undeletable:
30+
def __del__(self):
31+
1 / 0
32+
33+
def capture_envelope(self, envelope):
34+
print("capture_envelope was called")
35+
event = envelope.get_event()
36+
if event is not None:
37+
print(event)
38+
39+
transport.{transport}.capture_envelope = capture_envelope
40+
41+
init("http://foobar@localhost/123", integrations=[UnraisablehookIntegration()], {options})
42+
43+
undeletable = Undeletable()
44+
del undeletable
45+
""".format(
46+
transport=transport, options=options
47+
)
48+
)
49+
)
50+
51+
output = subprocess.check_output(
52+
[sys.executable, str(app)], stderr=subprocess.STDOUT
53+
)
54+
55+
assert b"ZeroDivisionError" in output
56+
assert b"capture_envelope was called" in output

tests/test_basics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,7 @@ def foo(event, hint):
870870
(["celery"], "sentry.python"),
871871
(["dedupe"], "sentry.python"),
872872
(["excepthook"], "sentry.python"),
873+
(["unraisablehook"], "sentry.python"),
873874
(["executing"], "sentry.python"),
874875
(["modules"], "sentry.python"),
875876
(["pure_eval"], "sentry.python"),

0 commit comments

Comments
 (0)