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.
- errorgap-0.1.1/CHANGELOG.md +9 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/PKG-INFO +4 -3
- {errorgap-0.1.0 → errorgap-0.1.1}/README.md +3 -2
- {errorgap-0.1.0 → errorgap-0.1.1}/pyproject.toml +1 -1
- errorgap-0.1.1/src/errorgap/backtrace.py +79 -0
- errorgap-0.1.1/src/errorgap/version.py +1 -0
- errorgap-0.1.1/tests/test_backtrace.py +81 -0
- errorgap-0.1.0/src/errorgap/backtrace.py +0 -45
- errorgap-0.1.0/src/errorgap/version.py +0 -1
- errorgap-0.1.0/tests/test_backtrace.py +0 -34
- {errorgap-0.1.0 → errorgap-0.1.1}/.github/workflows/ci.yml +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/.github/workflows/release.yml +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/.gitignore +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/LICENSE +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/__init__.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/client.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/configuration.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/django.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/fastapi.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/filter.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/flask.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/handlers.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/src/errorgap/notice.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/tests/conftest.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_client.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_configuration.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_django.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_fastapi.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_filter.py +0 -0
- {errorgap-0.1.0 → errorgap-0.1.1}/tests/test_flask.py +0 -0
- {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.
|
|
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
|
|
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()` |
|
|
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
|
|
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()` |
|
|
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 |
|
|
@@ -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
|
|
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
|