t-bug-catcher 0.3.1__tar.gz → 0.4.0__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.3.1 → t_bug_catcher-0.4.0}/PKG-INFO +2 -1
- t_bug_catcher-0.3.1/t_bug_catcher.egg-info/requires.txt → t_bug_catcher-0.4.0/requirements.txt +1 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/setup.cfg +1 -1
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/setup.py +1 -1
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/__init__.py +1 -5
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/bug_catcher.py +8 -1
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/config.py +1 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/jira.py +14 -2
- t_bug_catcher-0.4.0/t_bug_catcher/stack_saver.py +186 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/PKG-INFO +2 -1
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/SOURCES.txt +1 -0
- t_bug_catcher-0.3.1/requirements.txt → t_bug_catcher-0.4.0/t_bug_catcher.egg-info/requires.txt +2 -1
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/MANIFEST.in +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/README.rst +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/pyproject.toml +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/bug_snag.py +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/exceptions.py +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/utils/__init__.py +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/utils/common.py +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/utils/logger.py +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher/workitems.py +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/dependency_links.txt +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/not-zip-safe +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/top_level.txt +0 -0
- {t_bug_catcher-0.3.1 → t_bug_catcher-0.4.0}/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.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Bug catcher
|
|
5
5
|
Home-page: https://www.thoughtful.ai/
|
|
6
6
|
Author: Thoughtful
|
|
@@ -14,6 +14,7 @@ Requires-Python: >=3.9
|
|
|
14
14
|
Requires-Dist: requests<3.0.0,>=2.31.0
|
|
15
15
|
Requires-Dist: bugsnag>=4.6.1
|
|
16
16
|
Requires-Dist: retry~=0.9.2
|
|
17
|
+
Requires-Dist: whispers>=2.2.1
|
|
17
18
|
|
|
18
19
|
t-bug-catcher
|
|
19
20
|
==============
|
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
__author__ = """Thoughtful"""
|
|
4
4
|
__email__ = "support@thoughtful.ai"
|
|
5
5
|
# fmt: off
|
|
6
|
-
__version__ = '0.
|
|
6
|
+
__version__ = '0.4.0'
|
|
7
7
|
# fmt: on
|
|
8
8
|
|
|
9
9
|
from .bug_catcher import (
|
|
10
10
|
configure,
|
|
11
11
|
report_error,
|
|
12
|
-
report_error_to_jira,
|
|
13
|
-
report_error_to_bugsnag,
|
|
14
12
|
attach_file_to_exception,
|
|
15
13
|
install_sys_hook,
|
|
16
14
|
uninstall_sys_hook,
|
|
@@ -19,8 +17,6 @@ from .bug_catcher import (
|
|
|
19
17
|
__all__ = [
|
|
20
18
|
"configure",
|
|
21
19
|
"report_error",
|
|
22
|
-
"report_error_to_jira",
|
|
23
|
-
"report_error_to_bugsnag",
|
|
24
20
|
"attach_file_to_exception",
|
|
25
21
|
"install_sys_hook",
|
|
26
22
|
"uninstall_sys_hook",
|
|
@@ -8,6 +8,7 @@ from typing import List, Optional
|
|
|
8
8
|
from .bug_snag import BugSnag
|
|
9
9
|
from .config import CONFIG
|
|
10
10
|
from .jira import Jira
|
|
11
|
+
from .stack_saver import StackSaver
|
|
11
12
|
from .utils import logger
|
|
12
13
|
from .utils.common import get_frames
|
|
13
14
|
|
|
@@ -89,6 +90,7 @@ class BugCatcher:
|
|
|
89
90
|
self.__bug_snag: BugSnag = BugSnag()
|
|
90
91
|
self.__configurator: Configurator = Configurator(self.__jira, self.__bug_snag)
|
|
91
92
|
self.__sys_excepthook = None
|
|
93
|
+
self.__stack_saver = StackSaver()
|
|
92
94
|
|
|
93
95
|
@property
|
|
94
96
|
def configure(self):
|
|
@@ -131,11 +133,14 @@ class BugCatcher:
|
|
|
131
133
|
logger.warning(f"Exception {handled_error} already reported.")
|
|
132
134
|
return
|
|
133
135
|
|
|
136
|
+
stack_trace = self.__stack_saver.save_stack_trace(exception)
|
|
137
|
+
|
|
134
138
|
if self.__configurator.is_jira_configured:
|
|
135
139
|
self.__jira.report_error(
|
|
136
140
|
exception=exception,
|
|
137
141
|
assignee=assignee,
|
|
138
142
|
attachments=attachments,
|
|
143
|
+
stack_trace=stack_trace,
|
|
139
144
|
additional_info=description,
|
|
140
145
|
metadata=metadata,
|
|
141
146
|
)
|
|
@@ -243,8 +248,10 @@ class BugCatcher:
|
|
|
243
248
|
logger.warning(f"Exception {handled_error} already reported.")
|
|
244
249
|
return
|
|
245
250
|
|
|
251
|
+
stack_trace = self.__stack_saver.save_stack_trace(exc_value)
|
|
252
|
+
|
|
246
253
|
if self.__configurator.is_jira_configured:
|
|
247
|
-
self.__jira.report_unhandled_error(exc_type, exc_value, exc_traceback)
|
|
254
|
+
self.__jira.report_unhandled_error(exc_type, exc_value, exc_traceback, stack_trace)
|
|
248
255
|
if self.__configurator.is_bugsnag_configured:
|
|
249
256
|
self.__bug_snag.report_unhandled_error(exc_type, exc_value, exc_traceback)
|
|
250
257
|
|
|
@@ -20,6 +20,7 @@ class Config:
|
|
|
20
20
|
f"https://cloud.robocorp.com/organizations/{os.environ.get('RC_ORGANIZATION_ID')}"
|
|
21
21
|
f"/workspaces/{os.environ.get('RC_WORKSPACE_ID')}/processes"
|
|
22
22
|
f"/{os.environ.get('RC_PROCESS_ID')}/runs/{os.environ.get('RC_PROCESS_RUN_ID')}/"
|
|
23
|
+
f"stepRuns/{os.environ.get('RC_ACTIVITY_RUN_ID')}/"
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
ENVIRONMENT = (
|
|
@@ -959,6 +959,7 @@ class Jira:
|
|
|
959
959
|
exception: Optional[Exception] = None,
|
|
960
960
|
assignee: Optional[str] = None,
|
|
961
961
|
attachments: Union[List, str, Path, None] = None,
|
|
962
|
+
stack_trace: Optional[str] = None,
|
|
962
963
|
metadata: Optional[dict] = None,
|
|
963
964
|
additional_info: Optional[str] = None,
|
|
964
965
|
) -> dict:
|
|
@@ -968,6 +969,7 @@ class Jira:
|
|
|
968
969
|
exception (Exception, optional): The exception to be added to the Jira issue.
|
|
969
970
|
assignee (str, optional): The assignee to be added to the Jira issue.
|
|
970
971
|
attachments (List, optional): List of attachments to be added to the Jira issue.
|
|
972
|
+
stack_trace (str, optional): Stack trace to be added to the Jira issue.
|
|
971
973
|
metadata (dict, optional): Metadata to be added to the Jira issue.
|
|
972
974
|
additional_info (str, optional): Additional information to be added to the Jira issue.
|
|
973
975
|
|
|
@@ -1012,6 +1014,9 @@ class Jira:
|
|
|
1012
1014
|
)
|
|
1013
1015
|
return existing_ticket
|
|
1014
1016
|
|
|
1017
|
+
if stack_trace:
|
|
1018
|
+
attachments.insert(0, stack_trace)
|
|
1019
|
+
|
|
1015
1020
|
assignee_id = None
|
|
1016
1021
|
assignee = assignee if assignee else self._default_assignee
|
|
1017
1022
|
if assignee:
|
|
@@ -1036,19 +1041,23 @@ class Jira:
|
|
|
1036
1041
|
attachments=attachments,
|
|
1037
1042
|
labels=["bug_catcher"],
|
|
1038
1043
|
)
|
|
1044
|
+
if os.path.exists(stack_trace):
|
|
1045
|
+
os.remove(stack_trace)
|
|
1039
1046
|
return response
|
|
1040
1047
|
except Exception as ex:
|
|
1041
1048
|
logger.warning(f"Failed to create Jira issue due to: {ex.__class__.__name__}: {ex}")
|
|
1042
1049
|
return False
|
|
1043
1050
|
|
|
1044
|
-
def report_unhandled_error(
|
|
1051
|
+
def report_unhandled_error(
|
|
1052
|
+
self, exc_type: type, exc_value: Union[Exception, str], exc_traceback: TracebackType, stack_trace: str = None
|
|
1053
|
+
):
|
|
1045
1054
|
"""Report an unhandled error to Jira.
|
|
1046
1055
|
|
|
1047
1056
|
Args:
|
|
1048
1057
|
exc_type (type): The type of the exception.
|
|
1049
1058
|
exc_value (Exception, str): The value of the exception.
|
|
1050
1059
|
exc_traceback (TracebackType): The traceback of the exception.
|
|
1051
|
-
|
|
1060
|
+
stack_trace (str, optional): Stack trace to be added to the Jira issue.
|
|
1052
1061
|
|
|
1053
1062
|
Returns:
|
|
1054
1063
|
The response from creating the Jira issue.
|
|
@@ -1086,8 +1095,11 @@ class Jira:
|
|
|
1086
1095
|
summary=summary,
|
|
1087
1096
|
description=description,
|
|
1088
1097
|
assignee_id=assignee_id,
|
|
1098
|
+
attachments=[stack_trace] if stack_trace else None,
|
|
1089
1099
|
labels=["bug_catcher", "fatal_error"],
|
|
1090
1100
|
)
|
|
1101
|
+
if os.path.exists(stack_trace):
|
|
1102
|
+
os.remove(stack_trace)
|
|
1091
1103
|
return response
|
|
1092
1104
|
except Exception as ex:
|
|
1093
1105
|
logger.warning(f"Failed to create Jira issue due to: {ex.__class__.__name__}: {ex}")
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from json import JSONEncoder
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import FunctionType, ModuleType
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import whispers
|
|
13
|
+
|
|
14
|
+
from .utils import logger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Encoder(JSONEncoder):
|
|
18
|
+
"""Encoder class for encoding the Episode object to json."""
|
|
19
|
+
|
|
20
|
+
def default(self, o):
|
|
21
|
+
"""This method is used to encode the Episode object to json.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
o (object): The object to be encoded.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
str: The json string.
|
|
28
|
+
"""
|
|
29
|
+
if hasattr(o, "__dict__"):
|
|
30
|
+
return o.__dict__
|
|
31
|
+
if isinstance(o, datetime):
|
|
32
|
+
return o.isoformat()
|
|
33
|
+
if isinstance(o, Path):
|
|
34
|
+
return str(o)
|
|
35
|
+
return JSONEncoder.default(self, o)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class StackSaver:
|
|
39
|
+
"""A class to save the stack trace."""
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
"""Initializes the StackSaver class."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def strip_path(path: str):
|
|
47
|
+
"""A static method to strip the current working directory path from the input.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path (str): The path from which to strip the current working directory path.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
str: The stripped path.
|
|
54
|
+
"""
|
|
55
|
+
return path.replace(os.getcwd(), "").strip(os.sep)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def serialize_frame_info(frame_info: dict) -> dict:
|
|
59
|
+
"""A static method to serialize the frame info.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
frame_info (dict): The frame info to be serialized.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
dict: The serialized frame info.
|
|
66
|
+
"""
|
|
67
|
+
run_locals = {}
|
|
68
|
+
run_args = {}
|
|
69
|
+
if frame_info["locals"]:
|
|
70
|
+
for key, value in frame_info["locals"].items():
|
|
71
|
+
if isinstance(value, dict):
|
|
72
|
+
run_locals[str(key)] = value
|
|
73
|
+
else:
|
|
74
|
+
run_locals[str(key)] = str(value)
|
|
75
|
+
for key, value in frame_info["args"].items():
|
|
76
|
+
if isinstance(value, dict):
|
|
77
|
+
run_args[str(key)] = value
|
|
78
|
+
else:
|
|
79
|
+
run_args[str(key)] = str(value)
|
|
80
|
+
serializable_frame_info = {
|
|
81
|
+
"filename": frame_info["filename"],
|
|
82
|
+
"function_name": frame_info["function_name"],
|
|
83
|
+
"locals": run_locals,
|
|
84
|
+
"args": run_args,
|
|
85
|
+
}
|
|
86
|
+
return serializable_frame_info
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def filter_variables(variables: dict) -> dict:
|
|
90
|
+
"""A static method to filter the variables.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
variables (dict): The variables to be filtered.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
dict: The filtered variables.
|
|
97
|
+
"""
|
|
98
|
+
if not isinstance(variables, dict):
|
|
99
|
+
return variables
|
|
100
|
+
else:
|
|
101
|
+
local_variables = {}
|
|
102
|
+
for var_name, var in variables.items():
|
|
103
|
+
if re.match(r"^__\w+__$", var_name):
|
|
104
|
+
continue
|
|
105
|
+
if isinstance(var, (ModuleType, FunctionType)):
|
|
106
|
+
continue
|
|
107
|
+
local_variables[var_name] = var
|
|
108
|
+
return local_variables
|
|
109
|
+
|
|
110
|
+
def mask_credentials(self, file_path: str) -> None:
|
|
111
|
+
"""A method to mask the credentials in the file.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
file_path (str): The path of the file to be masked.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
Exception: If the masking fails.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
None
|
|
121
|
+
"""
|
|
122
|
+
with open(file_path, "r") as f:
|
|
123
|
+
filedata = f.readlines()
|
|
124
|
+
|
|
125
|
+
secrets = [secret for secret in whispers.secrets(file_path)]
|
|
126
|
+
|
|
127
|
+
for index, line in enumerate(filedata):
|
|
128
|
+
if not secrets:
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
for secret in secrets:
|
|
132
|
+
if secret.key in line and secret.value in line:
|
|
133
|
+
filedata[index] = line.replace(secret.value, secret.value[:1] + "***")
|
|
134
|
+
secrets.pop(secrets.index(secret))
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
if secrets:
|
|
138
|
+
logger.warning("Failed to mask credentials")
|
|
139
|
+
os.remove(file_path)
|
|
140
|
+
raise Exception("Failed to mask credentials")
|
|
141
|
+
|
|
142
|
+
with open(file_path, "w") as file:
|
|
143
|
+
file.writelines(filedata)
|
|
144
|
+
|
|
145
|
+
def save_stack_trace(self, exception: Optional[Exception] = None):
|
|
146
|
+
"""A method to save the stack trace.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
exception (Exception, optional): The exception to be saved. Defaults to None.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Optional[str]: The path of the saved stack trace.
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
frames = []
|
|
156
|
+
stack_details_json = []
|
|
157
|
+
tb = exception.__traceback__ if exception else sys.exc_info()[2]
|
|
158
|
+
while tb is not None:
|
|
159
|
+
frame = tb.tb_frame
|
|
160
|
+
if "site-packages" in frame.f_code.co_filename:
|
|
161
|
+
tb = tb.tb_next
|
|
162
|
+
continue
|
|
163
|
+
frames.append(frame)
|
|
164
|
+
tb = tb.tb_next
|
|
165
|
+
frames = frames[:3]
|
|
166
|
+
|
|
167
|
+
for frame in frames:
|
|
168
|
+
frame_info = {
|
|
169
|
+
"filename": self.strip_path(frame.f_code.co_filename),
|
|
170
|
+
"function_name": frame.f_code.co_name,
|
|
171
|
+
"locals": self.filter_variables(frame.f_locals),
|
|
172
|
+
"args": self.filter_variables(inspect.getargvalues(frame)[3]),
|
|
173
|
+
}
|
|
174
|
+
stack_details_json.append(self.serialize_frame_info(frame_info))
|
|
175
|
+
|
|
176
|
+
file_path = f"stack_details_{datetime.now().strftime('%Y%m%d%H%M%S')}.json"
|
|
177
|
+
|
|
178
|
+
with open(file_path, "w") as f:
|
|
179
|
+
json.dump(stack_details_json, f, indent=4, cls=_Encoder)
|
|
180
|
+
|
|
181
|
+
self.mask_credentials(file_path)
|
|
182
|
+
|
|
183
|
+
return file_path
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.warning(f"Failed to save stack trace: {e}")
|
|
186
|
+
return
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: t_bug_catcher
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Bug catcher
|
|
5
5
|
Home-page: https://www.thoughtful.ai/
|
|
6
6
|
Author: Thoughtful
|
|
@@ -14,6 +14,7 @@ Requires-Python: >=3.9
|
|
|
14
14
|
Requires-Dist: requests<3.0.0,>=2.31.0
|
|
15
15
|
Requires-Dist: bugsnag>=4.6.1
|
|
16
16
|
Requires-Dist: retry~=0.9.2
|
|
17
|
+
Requires-Dist: whispers>=2.2.1
|
|
17
18
|
|
|
18
19
|
t-bug-catcher
|
|
19
20
|
==============
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|