errorgap 0.1.0__tar.gz → 0.1.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 (31) hide show
  1. errorgap-0.1.1/CHANGELOG.md +9 -0
  2. {errorgap-0.1.0 → errorgap-0.1.1}/PKG-INFO +4 -3
  3. {errorgap-0.1.0 → errorgap-0.1.1}/README.md +3 -2
  4. {errorgap-0.1.0 → errorgap-0.1.1}/pyproject.toml +1 -1
  5. errorgap-0.1.1/src/errorgap/backtrace.py +79 -0
  6. errorgap-0.1.1/src/errorgap/version.py +1 -0
  7. errorgap-0.1.1/tests/test_backtrace.py +81 -0
  8. errorgap-0.1.0/src/errorgap/backtrace.py +0 -45
  9. errorgap-0.1.0/src/errorgap/version.py +0 -1
  10. errorgap-0.1.0/tests/test_backtrace.py +0 -34
  11. {errorgap-0.1.0 → errorgap-0.1.1}/.github/workflows/ci.yml +0 -0
  12. {errorgap-0.1.0 → errorgap-0.1.1}/.github/workflows/release.yml +0 -0
  13. {errorgap-0.1.0 → errorgap-0.1.1}/.gitignore +0 -0
  14. {errorgap-0.1.0 → errorgap-0.1.1}/LICENSE +0 -0
  15. {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/__init__.py +0 -0
  16. {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/client.py +0 -0
  17. {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/configuration.py +0 -0
  18. {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/django.py +0 -0
  19. {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/fastapi.py +0 -0
  20. {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/filter.py +0 -0
  21. {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/flask.py +0 -0
  22. {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/handlers.py +0 -0
  23. {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/notice.py +0 -0
  24. {errorgap-0.1.0 → errorgap-0.1.1}/tests/conftest.py +0 -0
  25. {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_client.py +0 -0
  26. {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_configuration.py +0 -0
  27. {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_django.py +0 -0
  28. {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_fastapi.py +0 -0
  29. {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_filter.py +0 -0
  30. {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_flask.py +0 -0
  31. {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_notice.py +0 -0
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 — 2026-07-18
4
+
5
+ - Include bounded inline source excerpts for readable application and dependency
6
+ traceback frames so
7
+ Errorgap can render highlighted source without a repository integration.
8
+ - Order traceback frames innermost-first so frame 0 and group fingerprints use
9
+ the actual exception site.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errorgap
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Python notifier for Errorgap error tracking.
5
5
  Project-URL: Homepage, https://github.com/errorgaphq/errorgap-python
6
6
  Project-URL: Source, https://github.com/errorgaphq/errorgap-python
@@ -37,7 +37,8 @@ Description-Content-Type: text/markdown
37
37
  # errorgap
38
38
 
39
39
  Python notifier for [Errorgap](https://errorgap.com). Captures exceptions,
40
- normalizes tracebacks, and ships notices to an Errorgap server. Single
40
+ normalizes tracebacks with bounded source excerpts for readable frames, and ships
41
+ notices to an Errorgap server. Single
41
42
  package covers plain Python, Django, Flask, and FastAPI.
42
43
 
43
44
  ## Install
@@ -125,7 +126,7 @@ app.add_middleware(ErrorgapMiddleware)
125
126
  | `project_id` | `ERRORGAP_PROJECT_ID` | Optional, embedded in payload |
126
127
  | `api_key` | `ERRORGAP_API_KEY` | Sent as `x-errorgap-project-key` |
127
128
  | `environment` | `ERRORGAP_ENVIRONMENT`, `ENV`, or `development` | |
128
- | `root_directory` | `os.getcwd()` | Used to mark frames as `in_app` |
129
+ | `root_directory` | `os.getcwd()` | Marks application frames; readable dependency frames also include bounded source excerpts |
129
130
  | `async_` | `True` | Background-thread delivery |
130
131
  | `logger` | `logging.getLogger("errorgap")` | Pass `None` to silence |
131
132
  | `filter_keys` | `("password", "token", ...)` | Substring match, case-insensitive |
@@ -1,7 +1,8 @@
1
1
  # errorgap
2
2
 
3
3
  Python notifier for [Errorgap](https://errorgap.com). Captures exceptions,
4
- normalizes tracebacks, and ships notices to an Errorgap server. Single
4
+ normalizes tracebacks with bounded source excerpts for readable frames, and ships
5
+ notices to an Errorgap server. Single
5
6
  package covers plain Python, Django, Flask, and FastAPI.
6
7
 
7
8
  ## Install
@@ -89,7 +90,7 @@ app.add_middleware(ErrorgapMiddleware)
89
90
  | `project_id` | `ERRORGAP_PROJECT_ID` | Optional, embedded in payload |
90
91
  | `api_key` | `ERRORGAP_API_KEY` | Sent as `x-errorgap-project-key` |
91
92
  | `environment` | `ERRORGAP_ENVIRONMENT`, `ENV`, or `development` | |
92
- | `root_directory` | `os.getcwd()` | Used to mark frames as `in_app` |
93
+ | `root_directory` | `os.getcwd()` | Marks application frames; readable dependency frames also include bounded source excerpts |
93
94
  | `async_` | `True` | Background-thread delivery |
94
95
  | `logger` | `logging.getLogger("errorgap")` | Pass `None` to silence |
95
96
  | `filter_keys` | `("password", "token", ...)` | Substring match, case-insensitive |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "errorgap"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Python notifier for Errorgap error tracking."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import traceback
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ SOURCE_CONTEXT_RADIUS = 6
8
+ MAX_SOURCE_LINE_CHARS = 400
9
+
10
+
11
+ def parse_backtrace(exc: BaseException, root_directory: str) -> List[Dict[str, Any]]:
12
+ frames: List[Dict[str, Any]] = []
13
+ # traceback.extract_tb returns oldest -> newest. Errorgap frame index 0 is
14
+ # the innermost frame where the exception was raised.
15
+ tb_frames = reversed(traceback.extract_tb(exc.__traceback__))
16
+
17
+ for index, frame in enumerate(tb_frames):
18
+ file = frame.filename
19
+ in_app = _is_in_app(file, root_directory)
20
+ normalized: Dict[str, Any] = {
21
+ "file": _relative_file(file, root_directory),
22
+ "line": frame.lineno,
23
+ "function": frame.name,
24
+ "in_app": in_app,
25
+ "index": index,
26
+ }
27
+ source = _source_excerpt(file, frame.lineno)
28
+ if source is not None:
29
+ normalized["source"] = source
30
+ frames.append(normalized)
31
+
32
+ return frames
33
+
34
+
35
+ def _relative_file(file: Optional[str], root: str) -> Optional[str]:
36
+ if not file:
37
+ return file
38
+ if not root:
39
+ return file
40
+ normalized = root if root.endswith(os.sep) else root + os.sep
41
+ if file.startswith(normalized):
42
+ return file[len(normalized):]
43
+ return file
44
+
45
+
46
+ def _is_in_app(file: Optional[str], root: str) -> bool:
47
+ if not file or not root:
48
+ return False
49
+ if "site-packages" in file:
50
+ return False
51
+ if file.startswith("<"):
52
+ return False
53
+ return file.startswith(root)
54
+
55
+
56
+ def _source_excerpt(file: str, line: int) -> Optional[Dict[str, Any]]:
57
+ if line < 1:
58
+ return None
59
+ start_line = max(1, line - SOURCE_CONTEXT_RADIUS)
60
+ end_line = line + SOURCE_CONTEXT_RADIUS
61
+ lines: List[str] = []
62
+ target_found = False
63
+
64
+ try:
65
+ with open(file, encoding="utf-8", errors="replace") as source_file:
66
+ for number, text in enumerate(source_file, start=1):
67
+ if number < start_line:
68
+ continue
69
+ if number > end_line:
70
+ break
71
+ lines.append(text.rstrip("\r\n")[:MAX_SOURCE_LINE_CHARS])
72
+ if number == line:
73
+ target_found = True
74
+ except OSError:
75
+ return None
76
+
77
+ if not lines or not target_found:
78
+ return None
79
+ return {"start_line": start_line, "lines": lines}
@@ -0,0 +1 @@
1
+ VERSION = "0.1.1"
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from errorgap.backtrace import (
6
+ MAX_SOURCE_LINE_CHARS,
7
+ SOURCE_CONTEXT_RADIUS,
8
+ _source_excerpt,
9
+ parse_backtrace,
10
+ )
11
+
12
+
13
+ def _raise_and_capture() -> Exception:
14
+ try:
15
+ raise ValueError("boom")
16
+ except Exception as exc:
17
+ return exc
18
+
19
+
20
+ def test_parses_frames():
21
+ exc = _raise_and_capture()
22
+ frames = parse_backtrace(exc, os.getcwd())
23
+ assert len(frames) >= 1
24
+ top = frames[0]
25
+ assert top["function"] == "_raise_and_capture"
26
+ assert top["line"] > 0
27
+ assert "test_backtrace" in (top["file"] or "")
28
+ assert top["index"] == 0
29
+
30
+
31
+ def test_in_app_marks_files_under_root():
32
+ exc = _raise_and_capture()
33
+ frames = parse_backtrace(exc, os.getcwd())
34
+ assert frames[0]["in_app"] is True
35
+
36
+
37
+ def test_empty_for_exception_without_traceback():
38
+ exc = ValueError("no tb")
39
+ assert parse_backtrace(exc, os.getcwd()) == []
40
+
41
+
42
+ def _inner_failure() -> None:
43
+ raise RuntimeError("inner boom")
44
+
45
+
46
+ def _outer_failure() -> None:
47
+ _inner_failure()
48
+
49
+
50
+ def test_innermost_frame_is_first_and_includes_source_context():
51
+ try:
52
+ _outer_failure()
53
+ except RuntimeError as exc:
54
+ frames = parse_backtrace(exc, os.getcwd())
55
+
56
+ assert frames[0]["function"] == "_inner_failure"
57
+ assert frames[1]["function"] == "_outer_failure"
58
+ source = frames[0]["source"]
59
+ target_offset = frames[0]["line"] - source["start_line"]
60
+ assert 'raise RuntimeError("inner boom")' in source["lines"][target_offset]
61
+
62
+
63
+ def test_attaches_source_to_readable_vendor_frames():
64
+ exc = _raise_and_capture()
65
+ frames = parse_backtrace(exc, "/path/that/does/not/contain/tests")
66
+ assert frames[0]["in_app"] is False
67
+ source = frames[0]["source"]
68
+ target_offset = frames[0]["line"] - source["start_line"]
69
+ assert 'raise ValueError("boom")' in source["lines"][target_offset]
70
+
71
+
72
+ def test_source_excerpt_is_bounded(tmp_path):
73
+ source_file = tmp_path / "app.py"
74
+ source_file.write_text("\n".join(["x" * 500] * 30), encoding="utf-8")
75
+
76
+ source = _source_excerpt(str(source_file), 15)
77
+
78
+ assert source is not None
79
+ assert source["start_line"] == 15 - SOURCE_CONTEXT_RADIUS
80
+ assert len(source["lines"]) == SOURCE_CONTEXT_RADIUS * 2 + 1
81
+ assert all(len(line) == MAX_SOURCE_LINE_CHARS for line in source["lines"])
@@ -1,45 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import traceback
5
- from typing import Any, Dict, List, Optional
6
-
7
-
8
- def parse_backtrace(exc: BaseException, root_directory: str) -> List[Dict[str, Any]]:
9
- frames: List[Dict[str, Any]] = []
10
- tb_frames = traceback.extract_tb(exc.__traceback__)
11
-
12
- for index, frame in enumerate(tb_frames):
13
- file = frame.filename
14
- frames.append(
15
- {
16
- "file": _relative_file(file, root_directory),
17
- "line": frame.lineno,
18
- "function": frame.name,
19
- "in_app": _is_in_app(file, root_directory),
20
- "index": index,
21
- }
22
- )
23
-
24
- return frames
25
-
26
-
27
- def _relative_file(file: Optional[str], root: str) -> Optional[str]:
28
- if not file:
29
- return file
30
- if not root:
31
- return file
32
- normalized = root if root.endswith(os.sep) else root + os.sep
33
- if file.startswith(normalized):
34
- return file[len(normalized):]
35
- return file
36
-
37
-
38
- def _is_in_app(file: Optional[str], root: str) -> bool:
39
- if not file or not root:
40
- return False
41
- if "site-packages" in file:
42
- return False
43
- if file.startswith("<"):
44
- return False
45
- return file.startswith(root)
@@ -1 +0,0 @@
1
- VERSION = "0.1.0"
@@ -1,34 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
-
5
- from errorgap.backtrace import parse_backtrace
6
-
7
-
8
- def _raise_and_capture() -> Exception:
9
- try:
10
- raise ValueError("boom")
11
- except Exception as exc:
12
- return exc
13
-
14
-
15
- def test_parses_frames():
16
- exc = _raise_and_capture()
17
- frames = parse_backtrace(exc, os.getcwd())
18
- assert len(frames) >= 1
19
- top = frames[0]
20
- assert top["function"] == "_raise_and_capture"
21
- assert top["line"] > 0
22
- assert "test_backtrace" in (top["file"] or "")
23
- assert top["index"] == 0
24
-
25
-
26
- def test_in_app_marks_files_under_root():
27
- exc = _raise_and_capture()
28
- frames = parse_backtrace(exc, os.getcwd())
29
- assert frames[0]["in_app"] is True
30
-
31
-
32
- def test_empty_for_exception_without_traceback():
33
- exc = ValueError("no tb")
34
- assert parse_backtrace(exc, os.getcwd()) == []
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