Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions sentry_sdk/integrations/unraisablehook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import sys

import sentry_sdk
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
)
from sentry_sdk.integrations import Integration

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Callable
from typing import Any


class UnraisablehookIntegration(Integration):
identifier = "unraisablehook"

@staticmethod
def setup_once():
# type: () -> None
sys.unraisablehook = _make_unraisable(sys.unraisablehook)


def _make_unraisable(old_unraisablehook):
# type: (Callable[[sys.UnraisableHookArgs], Any]) -> Callable[[sys.UnraisableHookArgs], Any]
def sentry_sdk_unraisablehook(unraisable):
# type: (sys.UnraisableHookArgs) -> None
integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration)

# Note: If we replace this with ensure_integration_enabled then
# we break the exceptiongroup backport;
# See: https://github.com/getsentry/sentry-python/issues/3097
if integration is None:
return old_unraisablehook(unraisable)

if unraisable.exc_value and unraisable.exc_traceback:
with capture_internal_exceptions():
event, hint = event_from_exception(
(
unraisable.exc_type,
unraisable.exc_value,
unraisable.exc_traceback,
),
client_options=sentry_sdk.get_client().options,
mechanism={"type": "unraisablehook", "handled": False},
)
sentry_sdk.capture_event(event, hint=hint)

return old_unraisablehook(unraisable)

return sentry_sdk_unraisablehook
56 changes: 56 additions & 0 deletions tests/integrations/unraisablehook/test_unraisablehook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pytest
import sys
import subprocess

from textwrap import dedent


TEST_PARAMETERS = [
("", "HttpTransport"),
('_experiments={"transport_http2": True}', "Http2Transport"),
]
Comment on lines +8 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to test this both with and without HTTP/2? Is the behavior of the integration any different?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is copied over from test_excepthook. I guess the test is more of an integration test as it checks that the unraisable exception goes all the way to the transport.

I'm happy to change this. Would you just use Http2Transport now that will be the default?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't realize we're doing the same in the excepthook tests. I guess fine to keep as is then.


minimum_python_38 = pytest.mark.skipif(
sys.version_info < (3, 8),
reason="The unraisable exception hook is only available in Python 3.8 and above.",
)


@minimum_python_38
@pytest.mark.parametrize("options, transport", TEST_PARAMETERS)
def test_unraisablehook(tmpdir, options, transport):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
from sentry_sdk import init, transport
from sentry_sdk.integrations.unraisablehook import UnraisablehookIntegration

class Undeletable:
def __del__(self):
1 / 0

def capture_envelope(self, envelope):
print("capture_envelope was called")
event = envelope.get_event()
if event is not None:
print(event)

transport.{transport}.capture_envelope = capture_envelope

init("http://foobar@localhost/123", integrations=[UnraisablehookIntegration()], {options})

undeletable = Undeletable()
del undeletable
""".format(
transport=transport, options=options
)
)
)

output = subprocess.check_output(
[sys.executable, str(app)], stderr=subprocess.STDOUT
)

assert b"ZeroDivisionError" in output
assert b"capture_envelope was called" in output
1 change: 1 addition & 0 deletions tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,7 @@ def foo(event, hint):
(["celery"], "sentry.python"),
(["dedupe"], "sentry.python"),
(["excepthook"], "sentry.python"),
(["unraisablehook"], "sentry.python"),
(["executing"], "sentry.python"),
(["modules"], "sentry.python"),
(["pure_eval"], "sentry.python"),
Expand Down