t-bug-catcher 0.6.13__tar.gz → 0.6.14__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 (27) hide show
  1. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/PKG-INFO +1 -1
  2. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/setup.cfg +1 -1
  3. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/setup.py +1 -1
  4. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/__init__.py +1 -1
  5. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/jira.py +39 -9
  6. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/PKG-INFO +1 -1
  7. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/SOURCES.txt +1 -0
  8. t_bug_catcher-0.6.14/tests/test_jira_grouping.py +112 -0
  9. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/MANIFEST.in +0 -0
  10. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/README.rst +0 -0
  11. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/pyproject.toml +0 -0
  12. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/requirements.txt +0 -0
  13. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/bug_catcher.py +0 -0
  14. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/bug_snag.py +0 -0
  15. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/config.py +0 -0
  16. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/exceptions.py +0 -0
  17. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/resources/whispers_config.yml +0 -0
  18. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/stack_saver.py +0 -0
  19. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/utils/__init__.py +0 -0
  20. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/utils/common.py +0 -0
  21. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/utils/logger.py +0 -0
  22. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/workitems.py +0 -0
  23. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/dependency_links.txt +0 -0
  24. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/not-zip-safe +0 -0
  25. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/requires.txt +0 -0
  26. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/top_level.txt +0 -0
  27. {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/tests/test_t_bug_catcher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t_bug_catcher
3
- Version: 0.6.13
3
+ Version: 0.6.14
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.6.13
2
+ current_version = 0.6.14
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.6.13",
29
+ version="0.6.14",
30
30
  zip_safe=False,
31
31
  install_requires=install_requirements,
32
32
  include_package_data=True,
@@ -3,7 +3,7 @@
3
3
  __author__ = """Thoughtful"""
4
4
  __email__ = "support@thoughtful.ai"
5
5
  # fmt: off
6
- __version__ = '0.6.13'
6
+ __version__ = '0.6.14'
7
7
  # fmt: on
8
8
 
9
9
  from .bug_catcher import (
@@ -164,6 +164,31 @@ class Jira:
164
164
  "Content-Type": "application/json",
165
165
  }
166
166
 
167
+ @staticmethod
168
+ def _adf_to_text(adf) -> str:
169
+ """Extract plaintext from Atlassian Document Format (ADF) nodes.
170
+
171
+ Args:
172
+ adf: ADF structure (dict/list) or a plain string.
173
+
174
+ Returns:
175
+ str: Concatenated text content.
176
+ """
177
+ if not adf:
178
+ return ""
179
+ if isinstance(adf, str):
180
+ return adf
181
+ if isinstance(adf, list):
182
+ return "".join(Jira._adf_to_text(item) for item in adf)
183
+ if isinstance(adf, dict):
184
+ # Prefer explicit text if present
185
+ text_value = adf.get("text")
186
+ if isinstance(text_value, str):
187
+ return text_value
188
+ # Otherwise, walk nested content
189
+ return Jira._adf_to_text(adf.get("content", []))
190
+ return ""
191
+
167
192
  @retry_if_bad_request
168
193
  def get_issues(self, project_key: Optional[str] = None) -> dict:
169
194
  """A function to get the issues using a Jira API.
@@ -1406,11 +1431,15 @@ class Jira:
1406
1431
  all_issues = self.get_issues()["issues"]
1407
1432
 
1408
1433
  if group_id:
1409
- existing_tickets = [
1410
- ticket
1411
- for ticket in all_issues
1412
- if ticket["fields"]["description"] and f"{error_id}-" in ticket["fields"]["description"]
1413
- ]
1434
+ search_prefix = f"Error string ID: {error_id}"
1435
+ existing_tickets = []
1436
+ for ticket in all_issues:
1437
+ description = ticket.get("fields", {}).get("description")
1438
+ if not description:
1439
+ continue
1440
+ description_text = self._adf_to_text(description)
1441
+ if search_prefix in description_text:
1442
+ existing_tickets.append(ticket)
1414
1443
  summary = self.__create_summary(
1415
1444
  type(exception),
1416
1445
  exception,
@@ -1724,13 +1753,14 @@ class Jira:
1724
1753
  Returns:
1725
1754
  dict or None: The matching ticket if found, otherwise None.
1726
1755
  """
1756
+ search_token = f"Error string ID: {error_id}"
1727
1757
  for ticket in all_tickets:
1728
- description = ticket.get("fields", {}).get("description", "")
1758
+ description = ticket.get("fields", {}).get("description")
1729
1759
  if not description:
1730
1760
  continue
1731
- if f"{error_id}_~" not in description:
1732
- continue
1733
- return ticket
1761
+ description_text = self._adf_to_text(description)
1762
+ if search_token in description_text:
1763
+ return ticket
1734
1764
 
1735
1765
  else:
1736
1766
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t_bug_catcher
3
- Version: 0.6.13
3
+ Version: 0.6.14
4
4
  Summary: Bug catcher
5
5
  Home-page: https://www.thoughtful.ai/
6
6
  Author: Thoughtful
@@ -22,4 +22,5 @@ t_bug_catcher/resources/whispers_config.yml
22
22
  t_bug_catcher/utils/__init__.py
23
23
  t_bug_catcher/utils/common.py
24
24
  t_bug_catcher/utils/logger.py
25
+ tests/test_jira_grouping.py
25
26
  tests/test_t_bug_catcher.py
@@ -0,0 +1,112 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ from t_bug_catcher.jira import Jira
5
+
6
+
7
+ class TestJiraGrouping(unittest.TestCase):
8
+ """Tests for Jira ADF parsing and ticket grouping logic."""
9
+
10
+ def setUp(self):
11
+ """Set up the test environment."""
12
+ self.jira = Jira()
13
+
14
+ def test_adf_to_text_extracts_error_id(self):
15
+ """Ensure ADF parser extracts plain text including the error id token."""
16
+ error_id = "abc123"
17
+ adf = {
18
+ "type": "doc",
19
+ "version": 1,
20
+ "content": [
21
+ {
22
+ "type": "paragraph",
23
+ "content": [
24
+ {"type": "text", "text": "Error string ID: "},
25
+ {"type": "text", "text": error_id, "marks": [{"type": "em"}]},
26
+ ],
27
+ }
28
+ ],
29
+ }
30
+
31
+ text = Jira._adf_to_text(adf)
32
+ self.assertIn(f"Error string ID: {error_id}", text)
33
+
34
+ def test_filter_tickets_matches_adf_description(self):
35
+ """filter_tickets should find a ticket when the error id is embedded in ADF description."""
36
+ error_id = "abc123"
37
+ ticket = {
38
+ "id": "10001",
39
+ "key": "TEST-1",
40
+ "fields": {
41
+ "description": {
42
+ "type": "doc",
43
+ "version": 1,
44
+ "content": [
45
+ {
46
+ "type": "paragraph",
47
+ "content": [
48
+ {"type": "text", "text": f"Error string ID: {error_id}"},
49
+ ],
50
+ }
51
+ ],
52
+ }
53
+ },
54
+ }
55
+
56
+ matched = self.jira.filter_tickets(all_tickets=[ticket], error_id=error_id)
57
+ self.assertIsNotNone(matched)
58
+ self.assertEqual(matched["key"], "TEST-1")
59
+
60
+ @patch.object(Jira, "_Jira__update_existing_ticket")
61
+ @patch.object(Jira, "_Jira__create_new_ticket")
62
+ @patch.object(Jira, "get_issues")
63
+ @patch.object(Jira, "_Jira__generate_error_id")
64
+ def test_report_error_groups_updates_existing(self, mock_gen_id, mock_get_issues, mock_create, mock_update):
65
+ """report_error should create once, then update existing when the same error is reported again."""
66
+ fixed_id = "deadbeef"
67
+ mock_gen_id.return_value = fixed_id
68
+
69
+ # First call: no issues present -> should create
70
+ # Second call: an issue present with matching error id in ADF -> should update
71
+ adf_issue = {
72
+ "id": "10002",
73
+ "key": "TEST-2",
74
+ "fields": {
75
+ "description": {
76
+ "type": "doc",
77
+ "version": 1,
78
+ "content": [
79
+ {
80
+ "type": "paragraph",
81
+ "content": [
82
+ {"type": "text", "text": f"Error string ID: {fixed_id}"},
83
+ ],
84
+ }
85
+ ],
86
+ }
87
+ },
88
+ }
89
+ mock_get_issues.side_effect = [
90
+ {"issues": []},
91
+ {"issues": [adf_issue]},
92
+ ]
93
+
94
+ # Prevent any network side effects
95
+ mock_create.return_value = MagicMock(status_code=201, json=lambda: {"key": "TEST-NEW", "id": "10010"})
96
+ mock_update.return_value = None
97
+
98
+ # Raise and catch the same exception twice to keep traceback and ids deterministic
99
+ for call_idx in range(2):
100
+ try:
101
+ d = {"a": 1}
102
+ _ = d["missing"]
103
+ except Exception as ex:
104
+ self.jira.report_error(exception=ex)
105
+
106
+ # Should create once and then update once
107
+ self.assertEqual(mock_create.call_count, 1)
108
+ self.assertEqual(mock_update.call_count, 1)
109
+
110
+
111
+ if __name__ == "__main__":
112
+ unittest.main()