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.
Files changed (26) hide show
  1. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/PKG-INFO +1 -1
  2. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/setup.cfg +1 -1
  3. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/setup.py +1 -1
  4. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/__init__.py +5 -1
  5. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/bug_catcher.py +97 -2
  6. t_bug_catcher-0.2.1/t_bug_catcher/bug_snag.py +92 -0
  7. t_bug_catcher-0.2.1/t_bug_catcher/config.py +29 -0
  8. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/jira.py +408 -230
  9. t_bug_catcher-0.2.1/t_bug_catcher/utils/common.py +17 -0
  10. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/PKG-INFO +1 -1
  11. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/SOURCES.txt +1 -0
  12. t_bug_catcher-0.1.6/t_bug_catcher/bug_snag.py +0 -72
  13. t_bug_catcher-0.1.6/t_bug_catcher/config.py +0 -12
  14. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/MANIFEST.in +0 -0
  15. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/README.rst +0 -0
  16. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/pyproject.toml +0 -0
  17. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/requirements.txt +0 -0
  18. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/exceptions.py +0 -0
  19. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/utils/__init__.py +0 -0
  20. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/utils/logger.py +0 -0
  21. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher/workitems.py +0 -0
  22. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/dependency_links.txt +0 -0
  23. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/not-zip-safe +0 -0
  24. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/requires.txt +0 -0
  25. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/t_bug_catcher.egg-info/top_level.txt +0 -0
  26. {t_bug_catcher-0.1.6 → t_bug_catcher-0.2.1}/tests/test_t_bug_catcher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: t_bug_catcher
3
- Version: 0.1.6
3
+ Version: 0.2.1
4
4
  Summary: Bug catcher
5
5
  Home-page: https://www.thoughtful.ai/
6
6
  Author: Thoughtful
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.1.6
2
+ current_version = 0.2.1
3
3
  commit = True
4
4
  tag = False
5
5
 
@@ -26,7 +26,7 @@ setup(
26
26
  packages=find_packages(include=["t_bug_catcher", "t_bug_catcher.*"]),
27
27
  test_suite="tests",
28
28
  url="https://www.thoughtful.ai/",
29
- version="0.1.6",
29
+ version="0.2.1",
30
30
  zip_safe=False,
31
31
  install_requires=install_requirements,
32
32
  )
@@ -3,7 +3,7 @@
3
3
  __author__ = """Thoughtful"""
4
4
  __email__ = "support@thoughtful.ai"
5
5
  # fmt: off
6
- __version__ = '0.1.6'
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 .workitems import variables
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 variables.get("environment", "local") == "local":
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()