t-bug-catcher 0.1.6__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/PKG-INFO +1 -1
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/setup.cfg +1 -1
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/setup.py +1 -1
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/__init__.py +5 -1
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/bug_catcher.py +97 -2
- t_bug_catcher-0.2.1/t_bug_catcher/bug_snag.py +92 -0
- t_bug_catcher-0.2.1/t_bug_catcher/config.py +29 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/jira.py +408 -230
- t_bug_catcher-0.2.1/t_bug_catcher/utils/common.py +17 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/PKG-INFO +1 -1
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/SOURCES.txt +1 -0
- t_bug_catcher-0.1.6/t_bug_catcher/bug_snag.py +0 -72
- t_bug_catcher-0.1.6/t_bug_catcher/config.py +0 -12
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/MANIFEST.in +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/README.rst +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/pyproject.toml +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/requirements.txt +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/exceptions.py +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/utils/__init__.py +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/utils/logger.py +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/workitems.py +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/dependency_links.txt +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/not-zip-safe +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/requires.txt +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/top_level.txt +0 -0
- {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/tests/test_t_bug_catcher.py +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
__author__ = """Thoughtful"""
|
|
4
4
|
__email__ = "support@thoughtful.ai"
|
|
5
5
|
# fmt: off
|
|
6
|
-
__version__ = '0.1
|
|
6
|
+
__version__ = '0.2.1'
|
|
7
7
|
# fmt: on
|
|
8
8
|
|
|
9
9
|
from .bug_catcher import (
|
|
@@ -12,6 +12,8 @@ from .bug_catcher import (
|
|
|
12
12
|
report_error_to_jira,
|
|
13
13
|
report_error_to_bugsnag,
|
|
14
14
|
attach_file_to_exception,
|
|
15
|
+
install_sys_hook,
|
|
16
|
+
uninstall_sys_hook,
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
__all__ = [
|
|
@@ -20,4 +22,6 @@ __all__ = [
|
|
|
20
22
|
"report_error_to_jira",
|
|
21
23
|
"report_error_to_bugsnag",
|
|
22
24
|
"attach_file_to_exception",
|
|
25
|
+
"install_sys_hook",
|
|
26
|
+
"uninstall_sys_hook",
|
|
23
27
|
]
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
"""JiraPoster class for interacting with the Jira API."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from types import TracebackType
|
|
3
6
|
from typing import List, Optional
|
|
4
7
|
|
|
5
8
|
from .bug_snag import BugSnag
|
|
9
|
+
from .config import CONFIG
|
|
6
10
|
from .jira import Jira
|
|
7
11
|
from .utils import logger
|
|
8
|
-
from .
|
|
12
|
+
from .utils.common import get_frames
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
class Configurator:
|
|
@@ -24,6 +28,7 @@ class Configurator:
|
|
|
24
28
|
api_token: str,
|
|
25
29
|
project_key: str,
|
|
26
30
|
webhook_url: Optional[str] = None,
|
|
31
|
+
default_assignee: Optional[str] = None,
|
|
27
32
|
):
|
|
28
33
|
"""Configures the JiraPoster and BugSnag classes.
|
|
29
34
|
|
|
@@ -32,6 +37,7 @@ class Configurator:
|
|
|
32
37
|
api_token (str): The API token for the Jira account.
|
|
33
38
|
project_key (str): The key of the Jira project.
|
|
34
39
|
webhook_url (str, optional): The webhook URL for the Jira project. Defaults to None.
|
|
40
|
+
default_assignee (str, optional): The default assignee for the Jira project. Defaults to None.
|
|
35
41
|
|
|
36
42
|
Returns:
|
|
37
43
|
None
|
|
@@ -41,6 +47,7 @@ class Configurator:
|
|
|
41
47
|
api_token=api_token,
|
|
42
48
|
project_key=project_key,
|
|
43
49
|
webhook_url=webhook_url,
|
|
50
|
+
default_assignee=default_assignee,
|
|
44
51
|
)
|
|
45
52
|
|
|
46
53
|
def bugsnag(self, api_key: str):
|
|
@@ -81,6 +88,7 @@ class BugCatcher:
|
|
|
81
88
|
self.__jira: Jira = Jira()
|
|
82
89
|
self.__bug_snag: BugSnag = BugSnag()
|
|
83
90
|
self.__configurator: Configurator = Configurator(self.__jira, self.__bug_snag)
|
|
91
|
+
self.__sys_excepthook = None
|
|
84
92
|
|
|
85
93
|
@property
|
|
86
94
|
def configure(self):
|
|
@@ -107,7 +115,7 @@ class BugCatcher:
|
|
|
107
115
|
Returns:
|
|
108
116
|
None
|
|
109
117
|
"""
|
|
110
|
-
if
|
|
118
|
+
if CONFIG.ENVIRONMENT.lower() == "local":
|
|
111
119
|
logger.warning("Reporting an error is not supported in local environment.")
|
|
112
120
|
return
|
|
113
121
|
|
|
@@ -115,6 +123,14 @@ class BugCatcher:
|
|
|
115
123
|
logger.warning("Jira and BugSnag are not configured. Please configure them before reporting an error.")
|
|
116
124
|
return
|
|
117
125
|
|
|
126
|
+
if not exception:
|
|
127
|
+
_, exception, _ = sys.exc_info()
|
|
128
|
+
|
|
129
|
+
handled_error = getattr(exception, "handled_error", None)
|
|
130
|
+
if handled_error:
|
|
131
|
+
logger.warning(f"Exception {handled_error} already reported.")
|
|
132
|
+
return
|
|
133
|
+
|
|
118
134
|
if self.__configurator.is_jira_configured:
|
|
119
135
|
self.__jira.report_error(
|
|
120
136
|
exception=exception,
|
|
@@ -130,6 +146,10 @@ class BugCatcher:
|
|
|
130
146
|
metadata=metadata,
|
|
131
147
|
)
|
|
132
148
|
|
|
149
|
+
frames = get_frames(exception.__traceback__)
|
|
150
|
+
exc_info = f"{os.path.basename(frames[-1].filename)}:{frames[-1].name}:{frames[-1].lineno}"
|
|
151
|
+
exception.handled_error = exc_info
|
|
152
|
+
|
|
133
153
|
def report_error_to_jira(
|
|
134
154
|
self,
|
|
135
155
|
exception: Optional[Exception] = None,
|
|
@@ -151,6 +171,10 @@ class BugCatcher:
|
|
|
151
171
|
None
|
|
152
172
|
|
|
153
173
|
"""
|
|
174
|
+
if CONFIG.ENVIRONMENT.lower() == "local":
|
|
175
|
+
logger.warning("Reporting an error is not supported in local environment.")
|
|
176
|
+
return
|
|
177
|
+
|
|
154
178
|
self.__jira.report_error(
|
|
155
179
|
exception=exception,
|
|
156
180
|
assignee=assignee,
|
|
@@ -170,6 +194,10 @@ class BugCatcher:
|
|
|
170
194
|
None
|
|
171
195
|
|
|
172
196
|
"""
|
|
197
|
+
if CONFIG.ENVIRONMENT.lower() == "local":
|
|
198
|
+
logger.warning("Reporting an error is not supported in local environment.")
|
|
199
|
+
return
|
|
200
|
+
|
|
173
201
|
self.__bug_snag.report_error(
|
|
174
202
|
exception=exception,
|
|
175
203
|
metadata=metadata,
|
|
@@ -191,11 +219,78 @@ class BugCatcher:
|
|
|
191
219
|
else:
|
|
192
220
|
exception.custom_attachments = [attachment]
|
|
193
221
|
|
|
222
|
+
def __excepthook(self, exc_type: type, exc_value: Exception, exc_traceback: TracebackType) -> None:
|
|
223
|
+
"""Handles unhandled exceptions.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
exc_type (type): The type of the exception.
|
|
227
|
+
exc_value (Exception): The value of the exception.
|
|
228
|
+
traceback (traceback): The traceback of the exception.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
None
|
|
232
|
+
"""
|
|
233
|
+
if CONFIG.ENVIRONMENT.lower() == "local":
|
|
234
|
+
logger.warning("Reporting an error is not supported in local environment.")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
if not self.__configurator.is_jira_configured and not self.__configurator.is_bugsnag_configured:
|
|
238
|
+
logger.warning("Jira and BugSnag are not configured. Please configure them before reporting an error.")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
handled_error = getattr(exc_value, "handled_error", None)
|
|
242
|
+
if handled_error:
|
|
243
|
+
logger.warning(f"Exception {handled_error} already reported.")
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
if self.__configurator.is_jira_configured:
|
|
247
|
+
self.__jira.report_unhandled_error(exc_type, exc_value, exc_traceback)
|
|
248
|
+
if self.__configurator.is_bugsnag_configured:
|
|
249
|
+
self.__bug_snag.report_unhandled_error(exc_type, exc_value, exc_traceback)
|
|
250
|
+
|
|
251
|
+
def __get_sys_hook_attribute(self, attribute: str = "bug_catcher_client"):
|
|
252
|
+
"""Checks if the system hook is installed.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
attribute (str, optional): The attribute to check. Defaults to "bug_catcher_client".
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The attribute of the system hook if it is installed, otherwise None.
|
|
259
|
+
"""
|
|
260
|
+
return getattr(sys.excepthook, attribute, None)
|
|
261
|
+
|
|
262
|
+
def install_sys_hook(self):
|
|
263
|
+
"""Installs a system hook to handle unhandled exceptions."""
|
|
264
|
+
if self.__get_sys_hook_attribute():
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
self.__sys_excepthook = sys.excepthook
|
|
268
|
+
|
|
269
|
+
def excepthook(*exc_info):
|
|
270
|
+
self.__excepthook(*exc_info)
|
|
271
|
+
|
|
272
|
+
if self.__sys_excepthook:
|
|
273
|
+
self.__sys_excepthook(*exc_info)
|
|
274
|
+
|
|
275
|
+
sys.excepthook = excepthook
|
|
276
|
+
sys.excepthook.bug_catcher_client = self
|
|
277
|
+
|
|
278
|
+
def uninstall_sys_hook(self):
|
|
279
|
+
"""Uninstalls the system hook to handle unhandled exceptions."""
|
|
280
|
+
client = self.__get_sys_hook_attribute()
|
|
281
|
+
|
|
282
|
+
if client is self and self.__sys_excepthook:
|
|
283
|
+
sys.excepthook = self.__sys_excepthook
|
|
284
|
+
self.__sys_excepthook = None
|
|
285
|
+
|
|
194
286
|
|
|
195
287
|
__bug_catcher = BugCatcher()
|
|
288
|
+
__bug_catcher.install_sys_hook()
|
|
196
289
|
|
|
197
290
|
configure = __bug_catcher.configure
|
|
198
291
|
report_error = __bug_catcher.report_error
|
|
199
292
|
attach_file_to_exception = __bug_catcher.attach_file_to_exception
|
|
200
293
|
report_error_to_jira = __bug_catcher.report_error_to_jira
|
|
201
294
|
report_error_to_bugsnag = __bug_catcher.report_error_to_bugsnag
|
|
295
|
+
install_sys_hook = __bug_catcher.install_sys_hook
|
|
296
|
+
uninstall_sys_hook = __bug_catcher.uninstall_sys_hook
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import bugsnag
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from .config import CONFIG
|
|
9
|
+
from .utils import logger
|
|
10
|
+
from .workitems import variables
|
|
11
|
+
|
|
12
|
+
bugsnag.configuration.auto_notify = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BugSnag:
|
|
16
|
+
"""BugSnag class for interacting with the BugSnag API."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initializes the BugSnag class."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def config(self, api_key: str) -> bool:
|
|
23
|
+
"""Configures the BugSnag class.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api_key (str): The API key for the BugSnag account.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
bool: True if the configuration was successful, False otherwise.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
bugsnag.configure(api_key=api_key, release_stage=CONFIG.ENVIRONMENT, auto_notify=False)
|
|
33
|
+
bugsnag.add_metadata_tab(
|
|
34
|
+
"Metadata",
|
|
35
|
+
{
|
|
36
|
+
"run_url": variables.get("processRunUrl", ""),
|
|
37
|
+
"run_by": variables.get("userEmail", ""),
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
response = requests.request(
|
|
41
|
+
"POST",
|
|
42
|
+
"https://otlp.bugsnag.com/v1/traces",
|
|
43
|
+
headers={
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
"Bugsnag-Api-Key": api_key,
|
|
46
|
+
"Bugsnag-Payload-Version": "4",
|
|
47
|
+
"Bugsnag-Sent-At": f"{datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f')}",
|
|
48
|
+
"Bugsnag-Span-Sampling": "True",
|
|
49
|
+
},
|
|
50
|
+
data='{"message": "test"}',
|
|
51
|
+
)
|
|
52
|
+
if response.status_code not in [200, 201, 202, 204]:
|
|
53
|
+
logger.warning(f"Error connecting to Bugsnag: {response.text}")
|
|
54
|
+
return False
|
|
55
|
+
return True
|
|
56
|
+
except Exception as ex:
|
|
57
|
+
logger.warning(f"Failed to configure Bugsnag: {ex}")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def report_error(self, exception: Optional[Exception] = None, metadata: Optional[dict] = None):
|
|
61
|
+
"""Sends an error to BugSnag.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
exception (Exception, optional): The exception to report.
|
|
65
|
+
metadata (dict, optional): The metadata to be added to the Bugsnag issue. Defaults to None.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
None
|
|
69
|
+
"""
|
|
70
|
+
if not exception:
|
|
71
|
+
_, exception, _ = sys.exc_info()
|
|
72
|
+
if isinstance(metadata, dict):
|
|
73
|
+
bugsnag.notify(exception=exception, metadata={"special_info": metadata})
|
|
74
|
+
return
|
|
75
|
+
if metadata is None:
|
|
76
|
+
bugsnag.notify(exception=exception)
|
|
77
|
+
return
|
|
78
|
+
logger.warning(f"Incorrect type of metadata: {type(metadata)}")
|
|
79
|
+
bugsnag.notify(exception=exception)
|
|
80
|
+
|
|
81
|
+
def report_unhandled_error(self, exc_type, exc_value, traceback):
|
|
82
|
+
"""Sends an unhandled exception to BugSnag.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
exc_type (type): The type of the exception.
|
|
86
|
+
exc_value (Exception): The value of the exception.
|
|
87
|
+
traceback (traceback): The traceback of the exception.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
None
|
|
91
|
+
"""
|
|
92
|
+
bugsnag.notify((exc_type, exc_value, traceback), severity="error")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .workitems import variables
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config:
|
|
7
|
+
"""Config class for configuring the application."""
|
|
8
|
+
|
|
9
|
+
class LIMITS:
|
|
10
|
+
"""Limits class for configuring the application."""
|
|
11
|
+
|
|
12
|
+
MAX_ATTACHMENTS: int = 5
|
|
13
|
+
MAX_ISSUE_ATTACHMENTS: int = 100
|
|
14
|
+
MAX_DESCRIPTION_LENGTH: int = 250
|
|
15
|
+
|
|
16
|
+
RC_RUN_LINK = (
|
|
17
|
+
f"https://cloud.robocorp.com/organizations/{os.environ.get('RC_ORGANIZATION_ID')}"
|
|
18
|
+
f"/workspaces/{os.environ.get('RC_WORKSPACE_ID')}/processes"
|
|
19
|
+
f"/{os.environ.get('RC_PROCESS_ID')}/runs/{os.environ.get('RC_PROCESS_RUN_ID')}/"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
ENVIRONMENT = (
|
|
23
|
+
"robocloud"
|
|
24
|
+
if not variables.get("environment") and os.environ.get("RC_PROCESS_RUN_ID")
|
|
25
|
+
else variables.get("environment", "local")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
CONFIG = Config()
|