Skip to content

Commit a7d2469

Browse files
feat(integrations): New SysExitIntegration (#3401)
* feat(integrations): New `SysExitIntegration` The `SysExitIntegration` reports `SystemExit` exceptions raised by calls made to `sys.exit` with a value indicating unsuccessful program termination – that is, any value other than `0` or `None`. Optionally, by setting `capture_successful_exits=True`, the `SysExitIntegration` can also report `SystemExit` exceptions resulting from `sys.exit` calls with successful values. You need to manually enable this integration if you wish to use it. Closes #2636 * Update sentry_sdk/integrations/sys_exit.py Co-authored-by: Anton Pirker <[email protected]> --------- Co-authored-by: Anton Pirker <[email protected]>
1 parent 4b361c5 commit a7d2469

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

sentry_sdk/integrations/sys_exit.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import sys
2+
3+
import sentry_sdk
4+
from sentry_sdk.utils import (
5+
ensure_integration_enabled,
6+
capture_internal_exceptions,
7+
event_from_exception,
8+
)
9+
from sentry_sdk.integrations import Integration
10+
from sentry_sdk._types import TYPE_CHECKING
11+
12+
if TYPE_CHECKING:
13+
from collections.abc import Callable
14+
from typing import NoReturn, Union
15+
16+
17+
class SysExitIntegration(Integration):
18+
"""Captures sys.exit calls and sends them as events to Sentry.
19+
20+
By default, SystemExit exceptions are not captured by the SDK. Enabling this integration will capture SystemExit
21+
exceptions generated by sys.exit calls and send them to Sentry.
22+
23+
This integration, in its default configuration, only captures the sys.exit call if the exit code is a non-zero and
24+
non-None value (unsuccessful exits). Pass `capture_successful_exits=True` to capture successful exits as well.
25+
Note that the integration does not capture SystemExit exceptions raised outside a call to sys.exit.
26+
"""
27+
28+
identifier = "sys_exit"
29+
30+
def __init__(self, *, capture_successful_exits=False):
31+
# type: (bool) -> None
32+
self._capture_successful_exits = capture_successful_exits
33+
34+
@staticmethod
35+
def setup_once():
36+
# type: () -> None
37+
SysExitIntegration._patch_sys_exit()
38+
39+
@staticmethod
40+
def _patch_sys_exit():
41+
# type: () -> None
42+
old_exit = sys.exit # type: Callable[[Union[str, int, None]], NoReturn]
43+
44+
@ensure_integration_enabled(SysExitIntegration, old_exit)
45+
def sentry_patched_exit(__status=0):
46+
# type: (Union[str, int, None]) -> NoReturn
47+
# @ensure_integration_enabled ensures that this is non-None
48+
integration = sentry_sdk.get_client().get_integration(
49+
SysExitIntegration
50+
) # type: SysExitIntegration
51+
52+
try:
53+
old_exit(__status)
54+
except SystemExit as e:
55+
with capture_internal_exceptions():
56+
if integration._capture_successful_exits or __status not in (
57+
0,
58+
None,
59+
):
60+
_capture_exception(e)
61+
raise e
62+
63+
sys.exit = sentry_patched_exit # type: ignore
64+
65+
66+
def _capture_exception(exc):
67+
# type: (SystemExit) -> None
68+
event, hint = event_from_exception(
69+
exc,
70+
client_options=sentry_sdk.get_client().options,
71+
mechanism={"type": SysExitIntegration.identifier, "handled": False},
72+
)
73+
sentry_sdk.capture_event(event, hint=hint)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import sys
2+
3+
import pytest
4+
5+
from sentry_sdk.integrations.sys_exit import SysExitIntegration
6+
7+
8+
@pytest.mark.parametrize(
9+
("integration_params", "exit_status", "should_capture"),
10+
(
11+
({}, 0, False),
12+
({}, 1, True),
13+
({}, None, False),
14+
({}, "unsuccessful exit", True),
15+
({"capture_successful_exits": False}, 0, False),
16+
({"capture_successful_exits": False}, 1, True),
17+
({"capture_successful_exits": False}, None, False),
18+
({"capture_successful_exits": False}, "unsuccessful exit", True),
19+
({"capture_successful_exits": True}, 0, True),
20+
({"capture_successful_exits": True}, 1, True),
21+
({"capture_successful_exits": True}, None, True),
22+
({"capture_successful_exits": True}, "unsuccessful exit", True),
23+
),
24+
)
25+
def test_sys_exit(
26+
sentry_init, capture_events, integration_params, exit_status, should_capture
27+
):
28+
sentry_init(integrations=[SysExitIntegration(**integration_params)])
29+
30+
events = capture_events()
31+
32+
# Manually catch the sys.exit rather than using pytest.raises because IDE does not recognize that pytest.raises
33+
# will catch SystemExit.
34+
try:
35+
sys.exit(exit_status)
36+
except SystemExit:
37+
...
38+
else:
39+
pytest.fail("Patched sys.exit did not raise SystemExit")
40+
41+
if should_capture:
42+
(event,) = events
43+
(exception_value,) = event["exception"]["values"]
44+
45+
assert exception_value["type"] == "SystemExit"
46+
assert exception_value["value"] == (
47+
str(exit_status) if exit_status is not None else ""
48+
)
49+
else:
50+
assert len(events) == 0
51+
52+
53+
def test_sys_exit_integration_not_auto_enabled(sentry_init, capture_events):
54+
sentry_init() # No SysExitIntegration
55+
56+
events = capture_events()
57+
58+
# Manually catch the sys.exit rather than using pytest.raises because IDE does not recognize that pytest.raises
59+
# will catch SystemExit.
60+
try:
61+
sys.exit(1)
62+
except SystemExit:
63+
...
64+
else:
65+
pytest.fail(
66+
"sys.exit should not be patched, but it must have been because it did not raise SystemExit"
67+
)
68+
69+
assert (
70+
len(events) == 0
71+
), "No events should have been captured because sys.exit should not have been patched"

0 commit comments

Comments
 (0)