bug-tracer-client 0.1.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.
@@ -0,0 +1,17 @@
1
+ .env
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+ .coverage
8
+ htmlcov/
9
+ .venv/
10
+ db.sqlite3
11
+ project/db.sqlite3
12
+ project/test_db.sqlite3
13
+ project/.pytest_cache/
14
+ project/.coverage
15
+ project/htmlcov/
16
+ project/.venv/
17
+ *.egg-info/
@@ -0,0 +1,9 @@
1
+ Proprietary License
2
+
3
+ Copyright (c) Bug Tracer.
4
+
5
+ All rights reserved.
6
+
7
+ No permission is granted to use, copy, modify, distribute, sublicense,
8
+ or publish this software except by explicit written agreement from the
9
+ copyright holder.
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: bug-tracer-client
3
+ Version: 0.1.0
4
+ Summary: Django middleware client for sending unhandled exceptions to Bug Tracer
5
+ Project-URL: Homepage, https://gitlab.com/bug-tracer/bug-tracer-api
6
+ Project-URL: Repository, https://gitlab.com/bug-tracer/bug-tracer-api
7
+ Project-URL: Issues, https://gitlab.com/bug-tracer/bug-tracer-api/-/issues
8
+ Author: Bug Tracer
9
+ License: Proprietary
10
+ License-File: LICENSE
11
+ Keywords: bug-tracker,django,errors,monitoring
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 5.1
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: Other/Proprietary License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: django<6.0,>=4.2
25
+ Requires-Dist: requests<3.0.0,>=2.32.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # bug-tracer-client
29
+
30
+ `bug-tracer-client` is a reusable Django app that captures unhandled exceptions and sends them to a Bug Tracer backend.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install bug-tracer-client
36
+ ```
37
+
38
+ ## Django setup
39
+
40
+ Add the app to `INSTALLED_APPS`:
41
+
42
+ ```python
43
+ INSTALLED_APPS = [
44
+ # ...
45
+ "bug_tracer_client",
46
+ ]
47
+ ```
48
+
49
+ Add the middleware after authentication middleware so authenticated user context can be included:
50
+
51
+ ```python
52
+ MIDDLEWARE = [
53
+ # ...
54
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
55
+ "bug_tracer_client.middleware.BugTracerMiddleware",
56
+ ]
57
+ ```
58
+
59
+ Set the single required setting:
60
+
61
+ ```python
62
+ BUG_TRACER_DSN = "bugtracer+https://PUBLIC_KEY@bug-tracer.example.com/1"
63
+ ```
64
+
65
+ Supported DSN formats:
66
+
67
+ - `bugtracer+https://<public_key>@<host>[:<port>]/<project_id>`
68
+ - `bugtracer+http://<public_key>@<host>[:<port>]/<project_id>`
69
+
70
+ ## What it sends
71
+
72
+ The middleware sends a best-effort JSON payload containing:
73
+
74
+ - exception type and message
75
+ - stacktrace frames
76
+ - request method and path
77
+ - optional authenticated user info
78
+
79
+ Sensitive fields such as `password`, `token`, `authorization`, and `cookie` are redacted before sending.
80
+
81
+ ## Smoke check
82
+
83
+ ```python
84
+ from bug_tracer_client import __version__
85
+
86
+ print(__version__)
87
+ ```
@@ -0,0 +1,60 @@
1
+ # bug-tracer-client
2
+
3
+ `bug-tracer-client` is a reusable Django app that captures unhandled exceptions and sends them to a Bug Tracer backend.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install bug-tracer-client
9
+ ```
10
+
11
+ ## Django setup
12
+
13
+ Add the app to `INSTALLED_APPS`:
14
+
15
+ ```python
16
+ INSTALLED_APPS = [
17
+ # ...
18
+ "bug_tracer_client",
19
+ ]
20
+ ```
21
+
22
+ Add the middleware after authentication middleware so authenticated user context can be included:
23
+
24
+ ```python
25
+ MIDDLEWARE = [
26
+ # ...
27
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
28
+ "bug_tracer_client.middleware.BugTracerMiddleware",
29
+ ]
30
+ ```
31
+
32
+ Set the single required setting:
33
+
34
+ ```python
35
+ BUG_TRACER_DSN = "bugtracer+https://PUBLIC_KEY@bug-tracer.example.com/1"
36
+ ```
37
+
38
+ Supported DSN formats:
39
+
40
+ - `bugtracer+https://<public_key>@<host>[:<port>]/<project_id>`
41
+ - `bugtracer+http://<public_key>@<host>[:<port>]/<project_id>`
42
+
43
+ ## What it sends
44
+
45
+ The middleware sends a best-effort JSON payload containing:
46
+
47
+ - exception type and message
48
+ - stacktrace frames
49
+ - request method and path
50
+ - optional authenticated user info
51
+
52
+ Sensitive fields such as `password`, `token`, `authorization`, and `cookie` are redacted before sending.
53
+
54
+ ## Smoke check
55
+
56
+ ```python
57
+ from bug_tracer_client import __version__
58
+
59
+ print(__version__)
60
+ ```
@@ -0,0 +1,3 @@
1
+ __version__ = "0.1.0"
2
+
3
+ default_app_config = "bug_tracer_client.apps.BugTracerClientConfig"
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class BugTracerClientConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "bug_tracer_client"
7
+ verbose_name = "Bug Tracer Client"
@@ -0,0 +1,22 @@
1
+ import requests
2
+
3
+ from .conf import BugTracerSettings, get_bug_tracer_settings
4
+
5
+
6
+ def send_event(payload: dict, config: BugTracerSettings | None = None, session=None) -> bool:
7
+ active_config = config or get_bug_tracer_settings()
8
+ if not active_config.parsed_dsn:
9
+ return False
10
+
11
+ http_client = session or requests
12
+ try:
13
+ response = http_client.post(
14
+ active_config.parsed_dsn.ingest_url,
15
+ json=payload,
16
+ headers={"X-Bug-Tracker-DSN": active_config.parsed_dsn.header_dsn},
17
+ timeout=active_config.timeout,
18
+ )
19
+ except requests.RequestException:
20
+ return False
21
+
22
+ return bool(getattr(response, "ok", False))
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass
2
+
3
+ from django.conf import settings
4
+
5
+ from .dsn import ParsedBugTracerDsn, parse_bug_tracer_dsn
6
+
7
+ DEFAULT_SENSITIVE_FIELDS = (
8
+ "password",
9
+ "passwd",
10
+ "secret",
11
+ "token",
12
+ "api_key",
13
+ "authorization",
14
+ "cookie",
15
+ )
16
+ DEFAULT_TIMEOUT = 1.0
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class BugTracerSettings:
21
+ parsed_dsn: ParsedBugTracerDsn | None
22
+ timeout: float
23
+ sensitive_fields: tuple[str, ...]
24
+
25
+
26
+ def get_bug_tracer_settings() -> BugTracerSettings:
27
+ raw_dsn = getattr(settings, "BUG_TRACER_DSN", "")
28
+ try:
29
+ parsed_dsn = parse_bug_tracer_dsn(raw_dsn) if raw_dsn else None
30
+ except ValueError:
31
+ parsed_dsn = None
32
+
33
+ return BugTracerSettings(
34
+ parsed_dsn=parsed_dsn,
35
+ timeout=DEFAULT_TIMEOUT,
36
+ sensitive_fields=DEFAULT_SENSITIVE_FIELDS,
37
+ )
@@ -0,0 +1,57 @@
1
+ from dataclasses import dataclass
2
+ from urllib.parse import urlsplit
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class ParsedBugTracerDsn:
7
+ ingest_url: str
8
+ header_dsn: str
9
+ public_key: str
10
+ host: str
11
+ project_id: str
12
+ transport: str
13
+
14
+
15
+ def parse_bug_tracer_dsn(raw_dsn: str) -> ParsedBugTracerDsn:
16
+ if not raw_dsn:
17
+ raise ValueError("BUG_TRACER_DSN is empty")
18
+
19
+ parsed = urlsplit(raw_dsn)
20
+ transport = _parse_transport(parsed.scheme)
21
+ public_key = parsed.username
22
+ host = parsed.hostname
23
+ project_id = _parse_project_id(parsed.path)
24
+
25
+ if not public_key:
26
+ raise ValueError("BUG_TRACER_DSN public key is required")
27
+ if parsed.password:
28
+ raise ValueError("BUG_TRACER_DSN password is not supported")
29
+ if not host:
30
+ raise ValueError("BUG_TRACER_DSN host is required")
31
+ if parsed.query or parsed.fragment:
32
+ raise ValueError("BUG_TRACER_DSN query and fragment are not supported")
33
+
34
+ netloc = parsed.netloc.rsplit("@", 1)[-1]
35
+ ingest_url = f"{transport}://{netloc}/api/v1/ingest/"
36
+ header_dsn = f"bugtracer://{public_key}@{netloc}/{project_id}"
37
+ return ParsedBugTracerDsn(
38
+ ingest_url=ingest_url,
39
+ header_dsn=header_dsn,
40
+ public_key=public_key,
41
+ host=host,
42
+ project_id=project_id,
43
+ transport=transport,
44
+ )
45
+
46
+
47
+ def _parse_transport(scheme: str) -> str:
48
+ if scheme not in {"bugtracer+http", "bugtracer+https"}:
49
+ raise ValueError("BUG_TRACER_DSN scheme must be bugtracer+http or bugtracer+https")
50
+ return scheme.removeprefix("bugtracer+")
51
+
52
+
53
+ def _parse_project_id(path: str) -> str:
54
+ project_id = path.lstrip("/")
55
+ if not project_id or "/" in project_id:
56
+ raise ValueError("BUG_TRACER_DSN path must contain a single project id")
57
+ return project_id
@@ -0,0 +1,34 @@
1
+ from collections.abc import Mapping
2
+
3
+ REDACTED = "[REDACTED]"
4
+
5
+
6
+ def sanitize_data(value, sensitive_fields: tuple[str, ...]):
7
+ normalized_sensitive_fields = {normalize_key(field) for field in sensitive_fields}
8
+ return _sanitize_value(value, normalized_sensitive_fields)
9
+
10
+
11
+ def _sanitize_value(value, sensitive_fields: set[str]):
12
+ if isinstance(value, Mapping):
13
+ return {
14
+ key: (
15
+ REDACTED
16
+ if is_sensitive_key(key, sensitive_fields)
17
+ else _sanitize_value(item, sensitive_fields)
18
+ )
19
+ for key, item in value.items()
20
+ }
21
+ if isinstance(value, list):
22
+ return [_sanitize_value(item, sensitive_fields) for item in value]
23
+ if isinstance(value, tuple):
24
+ return tuple(_sanitize_value(item, sensitive_fields) for item in value)
25
+ return value
26
+
27
+
28
+ def is_sensitive_key(key, sensitive_fields: set[str]) -> bool:
29
+ normalized_key = normalize_key(key)
30
+ return any(field and field in normalized_key for field in sensitive_fields)
31
+
32
+
33
+ def normalize_key(key) -> str:
34
+ return "".join(ch.lower() for ch in str(key) if ch.isalnum())
@@ -0,0 +1,23 @@
1
+ from .client import send_event
2
+ from .conf import get_bug_tracer_settings
3
+ from .payload import build_event_payload
4
+
5
+
6
+ class BugTracerMiddleware:
7
+ def __init__(self, get_response):
8
+ self.get_response = get_response
9
+
10
+ def __call__(self, request):
11
+ try:
12
+ return self.get_response(request)
13
+ except Exception as exc:
14
+ self.capture_exception(request, exc)
15
+ raise
16
+
17
+ def capture_exception(self, request, exc: Exception) -> None:
18
+ config = get_bug_tracer_settings()
19
+ try:
20
+ payload = build_event_payload(request, exc, config)
21
+ send_event(payload, config=config)
22
+ except Exception:
23
+ return
@@ -0,0 +1,97 @@
1
+ import json
2
+ import traceback
3
+ from datetime import datetime, timezone
4
+
5
+ from django.http import QueryDict
6
+
7
+ from .conf import BugTracerSettings
8
+ from .filters import sanitize_data
9
+
10
+
11
+ def build_event_payload(request, exc: Exception, config: BugTracerSettings) -> dict:
12
+ payload = {
13
+ "message": str(exc) or exc.__class__.__name__,
14
+ "timestamp": datetime.now(timezone.utc).isoformat(),
15
+ "exception": {
16
+ "type": exc.__class__.__name__,
17
+ "value": str(exc),
18
+ "stacktrace": {"frames": extract_frames(exc)},
19
+ },
20
+ "request": build_request_payload(request),
21
+ }
22
+ user_payload = build_user_payload(request)
23
+ if user_payload:
24
+ payload["user"] = user_payload
25
+ return sanitize_data(payload, config.sensitive_fields)
26
+
27
+
28
+ def extract_frames(exc: Exception) -> list[dict[str, object | None]]:
29
+ return [
30
+ {
31
+ "filename": frame.filename,
32
+ "function": frame.name,
33
+ "lineno": frame.lineno,
34
+ "context_line": frame.line,
35
+ }
36
+ for frame in traceback.extract_tb(exc.__traceback__)
37
+ ]
38
+
39
+
40
+ def build_request_payload(request) -> dict:
41
+ payload = {
42
+ "method": request.method,
43
+ "path": request.path,
44
+ "headers": build_headers(request),
45
+ }
46
+ query_params = querydict_to_dict(request.GET)
47
+ if query_params:
48
+ payload["query_params"] = query_params
49
+ body_data = build_request_data(request)
50
+ if body_data is not None:
51
+ payload["data"] = body_data
52
+ return payload
53
+
54
+
55
+ def build_headers(request) -> dict:
56
+ headers = {}
57
+ for key, value in request.META.items():
58
+ if key.startswith("HTTP_"):
59
+ header_name = key[5:].replace("_", "-").title()
60
+ headers[header_name] = value
61
+ elif key in {"CONTENT_TYPE", "CONTENT_LENGTH"}:
62
+ headers[key.replace("_", "-").title()] = value
63
+ return headers
64
+
65
+
66
+ def build_request_data(request):
67
+ if request.content_type == "application/json":
68
+ raw_body = request.body.decode("utf-8", errors="replace").strip()
69
+ if not raw_body:
70
+ return None
71
+ try:
72
+ return json.loads(raw_body)
73
+ except json.JSONDecodeError:
74
+ return {"raw_body": raw_body}
75
+ if request.POST:
76
+ return querydict_to_dict(request.POST)
77
+ return None
78
+
79
+
80
+ def build_user_payload(request) -> dict | None:
81
+ user = getattr(request, "user", None)
82
+ if not user or not getattr(user, "is_authenticated", False):
83
+ return None
84
+
85
+ payload = {"id": user.pk, "username": user.get_username()}
86
+ email = getattr(user, "email", "")
87
+ if email:
88
+ payload["email"] = email
89
+ return payload
90
+
91
+
92
+ def querydict_to_dict(querydict: QueryDict) -> dict:
93
+ data = {}
94
+ for key in querydict.keys():
95
+ values = querydict.getlist(key)
96
+ data[key] = values if len(values) > 1 else values[0]
97
+ return data
File without changes
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bug-tracer-client"
7
+ version = "0.1.0"
8
+ description = "Django middleware client for sending unhandled exceptions to Bug Tracer"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Proprietary" }
12
+ authors = [
13
+ { name = "Bug Tracer" }
14
+ ]
15
+ keywords = ["django", "errors", "bug-tracker", "monitoring"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Framework :: Django",
19
+ "Framework :: Django :: 5.1",
20
+ "Intended Audience :: Developers",
21
+ "License :: Other/Proprietary License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ ]
29
+ dependencies = [
30
+ "Django>=4.2,<6.0",
31
+ "requests>=2.32.0,<3.0.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://gitlab.com/bug-tracer/bug-tracer-api"
36
+ Repository = "https://gitlab.com/bug-tracer/bug-tracer-api"
37
+ Issues = "https://gitlab.com/bug-tracer/bug-tracer-api/-/issues"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["bug_tracer_client"]
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ include = [
44
+ "bug_tracer_client",
45
+ "README.md",
46
+ "LICENSE",
47
+ ]