autotrace-py 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,8 @@
1
+ include README.md
2
+ include pyproject.toml
3
+ recursive-include autotrace *.py
4
+
5
+ global-exclude *.py[cod] __pycache__ *.so *.dylib .DS_Store
6
+ global-exclude test_*.py *_test.py tests/*
7
+ global-exclude .env* *.env
8
+ global-exclude *.log
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: autotrace-py
3
+ Version: 0.1.0
4
+ Summary: AutoTrace Python SDK for autonomous error tracking and AI triage
5
+ Author-email: AutoTrace Team <maintainers@autotrace.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://autotrace.io
8
+ Project-URL: Repository, https://github.com/autotrace/autotrace-py
9
+ Project-URL: Documentation, https://docs.autotrace.io
10
+ Keywords: error-tracking,ai-triage,gemini,monitoring,exceptions
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: System :: Monitoring
21
+ Classifier: Topic :: Software Development :: Bug Tracking
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ Provides-Extra: django
25
+ Requires-Dist: django>=3.2; extra == "django"
26
+ Provides-Extra: fastapi
27
+ Requires-Dist: starlette>=0.14.0; extra == "fastapi"
28
+
29
+ # AutoTrace Python SDK (`autotrace-py`)
30
+
31
+ Lightweight, zero-dependency Python client for autonomous error tracking and AI triage with [AutoTrace](https://autotrace.io).
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install autotrace
37
+ # or from source:
38
+ pip install -e ./sdk/python
39
+ ```
40
+
41
+ ## Quickstart
42
+
43
+ ```python
44
+ import autotrace
45
+
46
+ # 1. Initialize once at application startup
47
+ autotrace.init(
48
+ api_key="<YOUR_API_KEY>",
49
+ endpoint_url="http://localhost:8000/api/ingest/",
50
+ environment="production",
51
+ context={"service": "payment-api", "version": "2.1.0"},
52
+ )
53
+
54
+ # 2. Manual Exception Capture
55
+ try:
56
+ process_order()
57
+ except Exception as exc:
58
+ autotrace.capture_exception(
59
+ exc_info=exc,
60
+ endpoint="/api/v1/orders",
61
+ context={"user_id": "usr_123"},
62
+ )
63
+ ```
64
+
65
+ ## Function Decorator
66
+
67
+ ```python
68
+ @autotrace.trace(endpoint="/jobs/sync-inventory")
69
+ def sync_inventory():
70
+ # If this raises, AutoTrace captures the error and traceback
71
+ perform_risky_task()
72
+ ```
73
+
74
+ ## Framework Integrations
75
+
76
+ ### Django
77
+
78
+ In your `settings.py`:
79
+ ```python
80
+ AUTOTRACE_API_KEY = "<YOUR_API_KEY>"
81
+ AUTOTRACE_ENDPOINT = "http://localhost:8000/api/ingest/"
82
+
83
+ MIDDLEWARE = [
84
+ # ... other middleware
85
+ "autotrace.integrations.django.AutoTraceMiddleware",
86
+ ]
87
+ ```
88
+
89
+ ### FastAPI / Starlette
90
+
91
+ In your `main.py`:
92
+ ```python
93
+ from fastapi import FastAPI
94
+ import autotrace
95
+ from autotrace.integrations.fastapi import AutoTraceMiddleware
96
+
97
+ autotrace.init(api_key="<YOUR_API_KEY>")
98
+
99
+ app = FastAPI()
100
+ app.add_middleware(AutoTraceMiddleware)
101
+ ```
@@ -0,0 +1,73 @@
1
+ # AutoTrace Python SDK (`autotrace-py`)
2
+
3
+ Lightweight, zero-dependency Python client for autonomous error tracking and AI triage with [AutoTrace](https://autotrace.io).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install autotrace
9
+ # or from source:
10
+ pip install -e ./sdk/python
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```python
16
+ import autotrace
17
+
18
+ # 1. Initialize once at application startup
19
+ autotrace.init(
20
+ api_key="<YOUR_API_KEY>",
21
+ endpoint_url="http://localhost:8000/api/ingest/",
22
+ environment="production",
23
+ context={"service": "payment-api", "version": "2.1.0"},
24
+ )
25
+
26
+ # 2. Manual Exception Capture
27
+ try:
28
+ process_order()
29
+ except Exception as exc:
30
+ autotrace.capture_exception(
31
+ exc_info=exc,
32
+ endpoint="/api/v1/orders",
33
+ context={"user_id": "usr_123"},
34
+ )
35
+ ```
36
+
37
+ ## Function Decorator
38
+
39
+ ```python
40
+ @autotrace.trace(endpoint="/jobs/sync-inventory")
41
+ def sync_inventory():
42
+ # If this raises, AutoTrace captures the error and traceback
43
+ perform_risky_task()
44
+ ```
45
+
46
+ ## Framework Integrations
47
+
48
+ ### Django
49
+
50
+ In your `settings.py`:
51
+ ```python
52
+ AUTOTRACE_API_KEY = "<YOUR_API_KEY>"
53
+ AUTOTRACE_ENDPOINT = "http://localhost:8000/api/ingest/"
54
+
55
+ MIDDLEWARE = [
56
+ # ... other middleware
57
+ "autotrace.integrations.django.AutoTraceMiddleware",
58
+ ]
59
+ ```
60
+
61
+ ### FastAPI / Starlette
62
+
63
+ In your `main.py`:
64
+ ```python
65
+ from fastapi import FastAPI
66
+ import autotrace
67
+ from autotrace.integrations.fastapi import AutoTraceMiddleware
68
+
69
+ autotrace.init(api_key="<YOUR_API_KEY>")
70
+
71
+ app = FastAPI()
72
+ app.add_middleware(AutoTraceMiddleware)
73
+ ```
@@ -0,0 +1,27 @@
1
+ """
2
+ AutoTrace Python SDK (autotrace-py)
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Autonomous error tracking and AI-powered incident triage.
5
+ """
6
+
7
+ from autotrace.client import (
8
+ AutoTraceClient,
9
+ capture_exception,
10
+ flush,
11
+ get_client,
12
+ init,
13
+ sanitize_data,
14
+ trace,
15
+ )
16
+
17
+ __all__ = [
18
+ "init",
19
+ "capture_exception",
20
+ "trace",
21
+ "flush",
22
+ "get_client",
23
+ "AutoTraceClient",
24
+ "sanitize_data",
25
+ ]
26
+
27
+ __version__ = "0.1.0"
@@ -0,0 +1,318 @@
1
+ """
2
+ AutoTrace Python SDK Client (autotrace-py)
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Lightweight, zero-dependency client for capturing and reporting
5
+ application exceptions asynchronously to the AutoTrace backend.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import atexit
11
+ import json
12
+ import logging
13
+ import os
14
+ import platform
15
+ import queue
16
+ import re
17
+ import sys
18
+ import threading
19
+ import traceback
20
+ import urllib.error
21
+ import urllib.request
22
+ from datetime import datetime, timezone
23
+ from functools import wraps
24
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
25
+
26
+ logger = logging.getLogger("autotrace")
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Constants & Defaults
30
+ # ---------------------------------------------------------------------------
31
+
32
+ DEFAULT_ENDPOINT = "http://localhost:8000/api/ingest/"
33
+ DEFAULT_ENVIRONMENT = "production"
34
+ TIMEOUT_SECONDS = 5.0
35
+
36
+ SENSITIVE_PATTERNS = re.compile(
37
+ r"(password|secret|token|authorization|api_key|access_token)", re.IGNORECASE
38
+ )
39
+ MASK_VALUE = "********"
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Data Sanitization
44
+ # ---------------------------------------------------------------------------
45
+
46
+ def _is_sensitive(key: str) -> bool:
47
+ return bool(SENSITIVE_PATTERNS.search(str(key)))
48
+
49
+
50
+ def sanitize_data(data: Any) -> Any:
51
+ """Recursively scrub sensitive keys in dictionaries and lists."""
52
+ if isinstance(data, dict):
53
+ return {
54
+ k: (MASK_VALUE if _is_sensitive(k) else sanitize_data(v))
55
+ for k, v in data.items()
56
+ }
57
+ elif isinstance(data, (list, tuple)):
58
+ return [sanitize_data(item) for item in data]
59
+ return data
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # AutoTrace Client
64
+ # ---------------------------------------------------------------------------
65
+
66
+ class AutoTraceClient:
67
+ """Core SDK client managing asynchronous dispatch of error events."""
68
+
69
+ def __init__(
70
+ self,
71
+ api_key: str,
72
+ endpoint_url: str = DEFAULT_ENDPOINT,
73
+ environment: str = DEFAULT_ENVIRONMENT,
74
+ default_context: Optional[Dict[str, Any]] = None,
75
+ max_queue_size: int = 1000,
76
+ ) -> None:
77
+ self.api_key = api_key.strip()
78
+ self.endpoint_url = endpoint_url
79
+ self.environment = environment
80
+ self.default_context = default_context or {}
81
+
82
+ # Asynchronous background worker queue
83
+ self._queue: queue.Queue = queue.Queue(maxsize=max_queue_size)
84
+ self._worker_thread = threading.Thread(
85
+ target=self._worker_loop,
86
+ daemon=True,
87
+ name="autotrace-worker",
88
+ )
89
+ self._worker_thread.start()
90
+
91
+ # Register exit handler to flush pending events
92
+ atexit.register(self.flush)
93
+
94
+ def _worker_loop(self) -> None:
95
+ """Background daemon thread that consumes and dispatches events."""
96
+ while True:
97
+ try:
98
+ payload, callback = self._queue.get()
99
+ if payload is None:
100
+ # Poison pill to shut down worker if needed
101
+ self._queue.task_done()
102
+ break
103
+ status_code, response_data = self._send_http(payload)
104
+ if callback:
105
+ callback(status_code, response_data)
106
+ self._queue.task_done()
107
+ except Exception as exc:
108
+ logger.debug("AutoTrace worker encountered an error (suppressed): %s", exc)
109
+
110
+ def _send_http(self, payload: Dict[str, Any]) -> Tuple[int, Optional[Dict[str, Any]]]:
111
+ """Perform a blocking HTTP POST with a short timeout. Fails silently."""
112
+ try:
113
+ body = json.dumps(payload).encode("utf-8")
114
+ req = urllib.request.Request(
115
+ self.endpoint_url,
116
+ data=body,
117
+ headers={
118
+ "Content-Type": "application/json",
119
+ "X-API-Key": self.api_key,
120
+ "User-Agent": f"autotrace-py/0.1.0 (Python {platform.python_version()})",
121
+ },
122
+ method="POST",
123
+ )
124
+ with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as resp:
125
+ status_code = resp.status
126
+ resp_text = resp.read().decode("utf-8", errors="replace")
127
+ try:
128
+ data = json.loads(resp_text)
129
+ except Exception:
130
+ data = {"raw": resp_text}
131
+ logger.debug("AutoTrace: event reported (HTTP %s)", status_code)
132
+ return status_code, data
133
+ except urllib.error.HTTPError as exc:
134
+ err_body = exc.read().decode("utf-8", errors="replace")
135
+ logger.debug("AutoTrace: API error HTTP %s: %s", exc.code, err_body)
136
+ return exc.code, {"error": err_body}
137
+ except Exception as exc:
138
+ logger.debug("AutoTrace: delivery failed (suppressed): %s", exc)
139
+ return 0, {"error": str(exc)}
140
+
141
+ def capture_exception(
142
+ self,
143
+ exc_info: Optional[Union[BaseException, Tuple[Any, ...], bool]] = None,
144
+ context: Optional[Dict[str, Any]] = None,
145
+ endpoint: Optional[str] = None,
146
+ sync: bool = False,
147
+ callback: Optional[Callable[[int, Optional[Dict[str, Any]]], None]] = None,
148
+ ) -> Optional[Tuple[int, Optional[Dict[str, Any]]]]:
149
+ """Capture and format an unhandled exception for AutoTrace ingestion."""
150
+ # 1. Resolve exception details
151
+ if exc_info is True or exc_info is None:
152
+ exc_type, exc_val, exc_tb = sys.exc_info()
153
+ elif isinstance(exc_info, BaseException):
154
+ exc_type, exc_val, exc_tb = type(exc_info), exc_info, exc_info.__traceback__
155
+ elif isinstance(exc_info, tuple) and len(exc_info) == 3:
156
+ exc_type, exc_val, exc_tb = exc_info
157
+ else:
158
+ exc_type, exc_val, exc_tb = None, None, None
159
+
160
+ if exc_val is None:
161
+ logger.debug("AutoTrace: capture_exception called with no active exception.")
162
+ return None
163
+
164
+ # 2. Format traceback array
165
+ tb_lines: List[str] = []
166
+ try:
167
+ raw_lines = traceback.format_exception(exc_type, exc_val, exc_tb)
168
+ for chunk in raw_lines:
169
+ tb_lines.extend(chunk.splitlines())
170
+ except Exception:
171
+ tb_lines = [f"{exc_type}: {exc_val}"]
172
+
173
+ # 3. Assemble runtime metadata
174
+ runtime_str = f"python {platform.python_version()}"
175
+
176
+ # 4. Merge context
177
+ merged_context: Dict[str, Any] = {
178
+ **self.default_context,
179
+ "environment": self.environment,
180
+ "os": f"{platform.system()} {platform.release()}",
181
+ "architecture": platform.machine(),
182
+ "timestamp": datetime.now(timezone.utc).isoformat(),
183
+ }
184
+ if context:
185
+ merged_context.update(context)
186
+
187
+ # 5. Build universal ingestion payload
188
+ payload = {
189
+ "error_type": exc_type.__name__ if exc_type else "Exception",
190
+ "error_message": str(exc_val),
191
+ "endpoint": endpoint or "",
192
+ "runtime": runtime_str,
193
+ "traceback": tb_lines,
194
+ "context": sanitize_data(merged_context),
195
+ }
196
+
197
+ # 6. Dispatch synchronously or queue for background thread
198
+ if sync:
199
+ return self._send_http(payload)
200
+
201
+ try:
202
+ self._queue.put_nowait((payload, callback))
203
+ except queue.Full:
204
+ logger.warning("AutoTrace: event queue is full, dropping error event.")
205
+
206
+ return None
207
+
208
+ def trace(
209
+ self,
210
+ endpoint: Optional[str] = None,
211
+ context: Optional[Dict[str, Any]] = None,
212
+ reraise: bool = False,
213
+ ) -> Callable:
214
+ """Decorator to automatically catch and report unhandled errors."""
215
+ def decorator(func: Callable) -> Callable:
216
+ @wraps(func)
217
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
218
+ try:
219
+ return func(*args, **kwargs)
220
+ except Exception as exc:
221
+ self.capture_exception(
222
+ exc_info=exc,
223
+ endpoint=endpoint or func.__qualname__,
224
+ context=context,
225
+ )
226
+ if reraise:
227
+ raise
228
+ return None
229
+ return wrapper
230
+ return decorator
231
+
232
+ def flush(self, timeout: float = 3.0) -> None:
233
+ """Block until all queued events are sent, up to *timeout* seconds."""
234
+ try:
235
+ self._queue.join()
236
+ except Exception:
237
+ pass
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Global Singleton Interface
242
+ # ---------------------------------------------------------------------------
243
+
244
+ _GLOBAL_CLIENT: Optional[AutoTraceClient] = None
245
+
246
+
247
+ def init(
248
+ api_key: str,
249
+ endpoint_url: str = DEFAULT_ENDPOINT,
250
+ environment: str = DEFAULT_ENVIRONMENT,
251
+ context: Optional[Dict[str, Any]] = None,
252
+ ) -> AutoTraceClient:
253
+ """Initialize the global AutoTrace client singleton."""
254
+ global _GLOBAL_CLIENT
255
+ _GLOBAL_CLIENT = AutoTraceClient(
256
+ api_key=api_key,
257
+ endpoint_url=endpoint_url,
258
+ environment=environment,
259
+ default_context=context,
260
+ )
261
+ logger.debug("AutoTrace SDK initialized for environment '%s'.", environment)
262
+ return _GLOBAL_CLIENT
263
+
264
+
265
+ def get_client() -> Optional[AutoTraceClient]:
266
+ """Retrieve the global AutoTrace client instance."""
267
+ return _GLOBAL_CLIENT
268
+
269
+
270
+ def capture_exception(
271
+ exc_info: Optional[Union[BaseException, Tuple[Any, ...], bool]] = None,
272
+ context: Optional[Dict[str, Any]] = None,
273
+ endpoint: Optional[str] = None,
274
+ sync: bool = False,
275
+ callback: Optional[Callable[[int, Optional[Dict[str, Any]]], None]] = None,
276
+ ) -> Optional[Tuple[int, Optional[Dict[str, Any]]]]:
277
+ """Capture an exception with the global client."""
278
+ if _GLOBAL_CLIENT is None:
279
+ logger.warning("AutoTrace SDK is not initialized. Call autotrace.init(...) first.")
280
+ return None
281
+ return _GLOBAL_CLIENT.capture_exception(
282
+ exc_info=exc_info,
283
+ context=context,
284
+ endpoint=endpoint,
285
+ sync=sync,
286
+ callback=callback,
287
+ )
288
+
289
+
290
+ def trace(
291
+ endpoint: Optional[str] = None,
292
+ context: Optional[Dict[str, Any]] = None,
293
+ reraise: bool = False,
294
+ ) -> Callable:
295
+ """Decorator to catch and report exceptions using the global client."""
296
+ def decorator(func: Callable) -> Callable:
297
+ @wraps(func)
298
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
299
+ try:
300
+ return func(*args, **kwargs)
301
+ except Exception as exc:
302
+ if _GLOBAL_CLIENT:
303
+ _GLOBAL_CLIENT.capture_exception(
304
+ exc_info=exc,
305
+ endpoint=endpoint or func.__qualname__,
306
+ context=context,
307
+ )
308
+ if reraise:
309
+ raise
310
+ return None
311
+ return wrapper
312
+ return decorator
313
+
314
+
315
+ def flush(timeout: float = 3.0) -> None:
316
+ """Flush pending events queued in the global client."""
317
+ if _GLOBAL_CLIENT:
318
+ _GLOBAL_CLIENT.flush(timeout=timeout)
@@ -0,0 +1 @@
1
+ """AutoTrace framework integrations."""
@@ -0,0 +1,77 @@
1
+ """
2
+ AutoTrace Django Integration Middleware
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Captures unhandled exceptions during Django request handling and reports
5
+ them to AutoTrace with sanitized request metadata and telemetry.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any, Callable, Dict, Optional
12
+
13
+ from autotrace.client import capture_exception, init, get_client
14
+
15
+
16
+ class AutoTraceMiddleware:
17
+ """Django middleware to automatically report unhandled exceptions."""
18
+
19
+ def __init__(self, get_response: Optional[Callable] = None) -> None:
20
+ self.get_response = get_response
21
+
22
+ # Auto-initialize if settings.AUTOTRACE_API_KEY is configured
23
+ try:
24
+ from django.conf import settings
25
+ if get_client() is None and hasattr(settings, "AUTOTRACE_API_KEY"):
26
+ init(
27
+ api_key=settings.AUTOTRACE_API_KEY,
28
+ endpoint_url=getattr(settings, "AUTOTRACE_ENDPOINT", "http://localhost:8000/api/ingest/"),
29
+ environment=getattr(settings, "AUTOTRACE_ENVIRONMENT", "production"),
30
+ )
31
+ except Exception:
32
+ pass
33
+
34
+ def __call__(self, request: Any) -> Any:
35
+ try:
36
+ response = self.get_response(request) if self.get_response else None
37
+ return response
38
+ except Exception as exc:
39
+ self.process_exception(request, exc)
40
+ raise
41
+
42
+ def process_exception(self, request: Any, exception: BaseException) -> None:
43
+ """Capture request telemetry and dispatch error event to AutoTrace."""
44
+ headers: Dict[str, str] = {}
45
+ for key, value in getattr(request, "META", {}).items():
46
+ if key.startswith("HTTP_"):
47
+ header_name = key[5:].replace("_", "-").title()
48
+ headers[header_name] = str(value)
49
+ elif key in ("CONTENT_TYPE", "CONTENT_LENGTH", "REMOTE_ADDR"):
50
+ headers[key.replace("_", "-").title()] = str(value)
51
+
52
+ # Parse request query params
53
+ query_params = dict(getattr(request, "GET", {}).items())
54
+
55
+ # Best-effort parse body payload
56
+ body_data: Any = None
57
+ try:
58
+ raw_body = getattr(request, "body", None)
59
+ if raw_body:
60
+ body_data = json.loads(raw_body.decode("utf-8"))
61
+ except Exception:
62
+ pass
63
+
64
+ context: Dict[str, Any] = {
65
+ "method": getattr(request, "method", "UNKNOWN"),
66
+ "path": getattr(request, "path", ""),
67
+ "headers": headers,
68
+ "query_params": query_params,
69
+ "body": body_data,
70
+ "user": str(getattr(request, "user", "anonymous")),
71
+ }
72
+
73
+ capture_exception(
74
+ exc_info=exception,
75
+ endpoint=getattr(request, "path", ""),
76
+ context=context,
77
+ )
@@ -0,0 +1,55 @@
1
+ """
2
+ AutoTrace FastAPI / Starlette Integration Middleware
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ ASGI middleware that intercepts unhandled exceptions in FastAPI / Starlette
5
+ applications, captures route & header metadata, and reports them to AutoTrace.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Callable, Dict
11
+
12
+ from autotrace.client import capture_exception
13
+
14
+
15
+ class AutoTraceMiddleware:
16
+ """ASGI middleware for FastAPI and Starlette applications."""
17
+
18
+ def __init__(self, app: Callable) -> None:
19
+ self.app = app
20
+
21
+ async def __call__(self, scope: Dict[str, Any], receive: Callable, send: Callable) -> None:
22
+ if scope["type"] != "http":
23
+ await self.app(scope, receive, send)
24
+ return
25
+
26
+ try:
27
+ await self.app(scope, receive, send)
28
+ except Exception as exc:
29
+ # Extract headers from ASGI scope
30
+ headers: Dict[str, str] = {}
31
+ for raw_k, raw_v in scope.get("headers", []):
32
+ k = raw_k.decode("latin1").lower()
33
+ v = raw_v.decode("latin1")
34
+ headers[k] = v
35
+
36
+ endpoint = scope.get("path", "")
37
+ method = scope.get("method", "GET")
38
+ query_string = scope.get("query_string", b"").decode("latin1")
39
+
40
+ context = {
41
+ "method": method,
42
+ "path": endpoint,
43
+ "query_string": query_string,
44
+ "headers": headers,
45
+ "client": scope.get("client"),
46
+ "server": scope.get("server"),
47
+ }
48
+
49
+ capture_exception(
50
+ exc_info=exc,
51
+ endpoint=endpoint,
52
+ context=context,
53
+ )
54
+ # Re-raise so FastAPI's error handlers / status responses continue
55
+ raise
@@ -0,0 +1,284 @@
1
+ """
2
+ AutoTrace Django Middleware — Client SDK
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ Drop-in Django middleware that captures unhandled exceptions and reports
6
+ them to the AutoTrace ingestion API.
7
+
8
+ Quick start
9
+ -----------
10
+ 1. ``pip install autotrace`` (or add this package to your requirements)
11
+
12
+ 2. Add to your Django settings::
13
+
14
+ MIDDLEWARE = [
15
+ ...
16
+ 'autotrace.middleware.AutoTraceMiddleware',
17
+ ]
18
+
19
+ AUTOTRACE_DSN = "https://api.autotrace.io/ingest/" # default
20
+ AUTOTRACE_API_KEY = "<your-project-api-key>"
21
+
22
+ 3. That's it — unhandled 500s are now reported automatically.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import logging
29
+ import re
30
+ import traceback
31
+ from datetime import datetime, timezone
32
+ from typing import Any, Callable
33
+
34
+ from django.conf import settings
35
+ from django.http import HttpRequest, HttpResponse
36
+
37
+ try:
38
+ import urllib.request
39
+ import urllib.error
40
+
41
+ _HAS_URLLIB = True
42
+ except ImportError: # pragma: no cover — paranoia guard
43
+ _HAS_URLLIB = False
44
+
45
+ logger = logging.getLogger("autotrace.middleware")
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Configuration helpers
49
+ # ---------------------------------------------------------------------------
50
+
51
+ # Default ingestion endpoint
52
+ _DEFAULT_DSN = "https://api.autotrace.io/ingest/"
53
+
54
+ # Maximum traceback frames forwarded (keeps payloads focused)
55
+ _MAX_TRACEBACK_FRAMES = 15
56
+
57
+ # Keys whose *values* will be masked in headers / body payloads.
58
+ # Matching is case-insensitive and checks whether the key *contains* the word.
59
+ _SENSITIVE_PATTERNS: re.Pattern[str] = re.compile(
60
+ r"(password|secret|token|authorization)", re.IGNORECASE
61
+ )
62
+
63
+ _MASK = "********"
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Data scrubbing
68
+ # ---------------------------------------------------------------------------
69
+
70
+
71
+ def _is_sensitive_key(key: str) -> bool:
72
+ """Return ``True`` if *key* looks like it holds a sensitive value."""
73
+ return bool(_SENSITIVE_PATTERNS.search(key))
74
+
75
+
76
+ def _scrub(data: Any) -> Any:
77
+ """Recursively walk *data* and mask values behind sensitive keys.
78
+
79
+ Handles nested dicts and lists. Everything else passes through
80
+ unchanged.
81
+ """
82
+ if isinstance(data, dict):
83
+ sanitised: dict[str, Any] = {}
84
+ for key, value in data.items():
85
+ if _is_sensitive_key(str(key)):
86
+ sanitised[key] = _MASK
87
+ else:
88
+ sanitised[key] = _scrub(value)
89
+ return sanitised
90
+
91
+ if isinstance(data, (list, tuple)):
92
+ return [_scrub(item) for item in data]
93
+
94
+ return data
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Request helpers
99
+ # ---------------------------------------------------------------------------
100
+
101
+
102
+ def _extract_headers(request: HttpRequest) -> dict[str, str]:
103
+ """Pull HTTP headers from ``request.META``, scrubbing sensitive ones."""
104
+ headers: dict[str, str] = {}
105
+ for meta_key, meta_value in request.META.items():
106
+ if not meta_key.startswith("HTTP_"):
107
+ continue
108
+ # Convert META key (e.g. HTTP_ACCEPT_LANGUAGE) → header name
109
+ header_name = meta_key[5:].replace("_", "-").title()
110
+ if _is_sensitive_key(header_name):
111
+ headers[header_name] = _MASK
112
+ else:
113
+ headers[header_name] = str(meta_value)
114
+ return headers
115
+
116
+
117
+ def _extract_body(request: HttpRequest) -> dict[str, Any]:
118
+ """Best-effort parse + scrub of the request body."""
119
+ try:
120
+ raw = request.body
121
+ if not raw:
122
+ return {}
123
+ payload = json.loads(raw.decode("utf-8"))
124
+ except Exception:
125
+ # Body might be form-encoded, binary, or otherwise unparsable.
126
+ # Wrap it as a raw string so we still capture *something*.
127
+ try:
128
+ payload = {"_raw": request.body.decode("utf-8", errors="replace")}
129
+ except Exception:
130
+ return {}
131
+
132
+ return _scrub(payload)
133
+
134
+
135
+ def _extract_traceback(exc: BaseException) -> list[str]:
136
+ """Return a compact, relevant traceback as a list of formatted lines.
137
+
138
+ We limit output to ``_MAX_TRACEBACK_FRAMES`` innermost frames so that
139
+ payloads stay small and focused on application code rather than deep
140
+ framework internals.
141
+ """
142
+ tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
143
+ # ``format_exception`` returns a list of multi-line strings; flatten.
144
+ flat: list[str] = []
145
+ for chunk in tb_lines:
146
+ flat.extend(chunk.splitlines())
147
+
148
+ # Keep only the tail (most relevant frames) + the final exception line.
149
+ if len(flat) > _MAX_TRACEBACK_FRAMES:
150
+ flat = ["... (truncated)"] + flat[-_MAX_TRACEBACK_FRAMES:]
151
+
152
+ return flat
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Payload construction
157
+ # ---------------------------------------------------------------------------
158
+
159
+
160
+ def _build_payload(
161
+ request: HttpRequest, exception: BaseException
162
+ ) -> dict[str, Any]:
163
+ """Assemble the JSON-serialisable error payload."""
164
+ return {
165
+ "exception": {
166
+ "type": type(exception).__qualname__,
167
+ "message": str(exception),
168
+ "traceback": _extract_traceback(exception),
169
+ },
170
+ "request": {
171
+ "method": request.method,
172
+ "path": request.path,
173
+ "query_string": request.META.get("QUERY_STRING", ""),
174
+ "headers": _extract_headers(request),
175
+ "body": _extract_body(request),
176
+ },
177
+ "server": {
178
+ "server_name": request.META.get("SERVER_NAME", ""),
179
+ "server_port": request.META.get("SERVER_PORT", ""),
180
+ },
181
+ "timestamp": datetime.now(timezone.utc).isoformat(),
182
+ "sdk": {
183
+ "name": "autotrace-python",
184
+ "version": "0.1.0",
185
+ },
186
+ }
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Transport
191
+ # ---------------------------------------------------------------------------
192
+
193
+
194
+ def _send_payload(payload: dict[str, Any], dsn: str, api_key: str) -> None:
195
+ """POST *payload* to the AutoTrace ingestion endpoint.
196
+
197
+ Uses only the stdlib ``urllib`` so we avoid adding ``requests`` as a
198
+ hard dependency for SDK consumers.
199
+
200
+ This function is wrapped in a blanket ``try / except`` at the call
201
+ site so that transport failures **never** propagate to the host app.
202
+ """
203
+ body = json.dumps(payload).encode("utf-8")
204
+ req = urllib.request.Request(
205
+ dsn,
206
+ data=body,
207
+ headers={
208
+ "Content-Type": "application/json",
209
+ "X-AutoTrace-Key": api_key,
210
+ },
211
+ method="POST",
212
+ )
213
+ urllib.request.urlopen(req, timeout=5)
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # Middleware
218
+ # ---------------------------------------------------------------------------
219
+
220
+
221
+ class AutoTraceMiddleware:
222
+ """Django middleware that reports unhandled exceptions to AutoTrace.
223
+
224
+ Configuration is read from ``django.conf.settings``:
225
+
226
+ ======================== ======================================
227
+ Setting Description
228
+ ======================== ======================================
229
+ ``AUTOTRACE_DSN`` Ingestion URL (default: ``https://api.autotrace.io/ingest/``)
230
+ ``AUTOTRACE_API_KEY`` Project API key (required)
231
+ ``AUTOTRACE_ENABLED`` Kill-switch — set ``False`` to disable (default: ``True``)
232
+ ======================== ======================================
233
+ """
234
+
235
+ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
236
+ self.get_response = get_response
237
+
238
+ # Read configuration once at startup.
239
+ self.dsn: str = getattr(settings, "AUTOTRACE_DSN", _DEFAULT_DSN)
240
+ self.api_key: str = getattr(settings, "AUTOTRACE_API_KEY", "")
241
+ self.enabled: bool = getattr(settings, "AUTOTRACE_ENABLED", True)
242
+
243
+ if self.enabled and not self.api_key:
244
+ logger.warning(
245
+ "AutoTraceMiddleware is enabled but AUTOTRACE_API_KEY is not "
246
+ "set. Error reports will be sent without authentication."
247
+ )
248
+
249
+ # -- Django middleware interface ------------------------------------------
250
+
251
+ def __call__(self, request: HttpRequest) -> HttpResponse:
252
+ """Wrap the downstream middleware / view chain."""
253
+ try:
254
+ response = self.get_response(request)
255
+ except Exception as exc:
256
+ # Capture → report → re-raise so Django's error handling
257
+ # (DEBUG page, logging, etc.) still works normally.
258
+ self._report(request, exc)
259
+ raise
260
+
261
+ return response
262
+
263
+ # -- Internal helpers ----------------------------------------------------
264
+
265
+ def _report(self, request: HttpRequest, exception: BaseException) -> None:
266
+ """Build and ship the error payload — silently swallowing any errors
267
+ that occur during reporting so we never break the host application.
268
+ """
269
+ if not self.enabled:
270
+ return
271
+
272
+ try:
273
+ payload = _build_payload(request, exception)
274
+ _send_payload(payload, self.dsn, self.api_key)
275
+ logger.debug("AutoTrace: error reported successfully.")
276
+ except Exception:
277
+ # ----- FAIL SILENTLY -----
278
+ # If AutoTrace's servers are unreachable, misconfigured, or the
279
+ # payload can't be serialised, we absolutely must not let that
280
+ # crash the client's application a *second* time.
281
+ logger.debug(
282
+ "AutoTrace: failed to report error (suppressed).",
283
+ exc_info=True,
284
+ )
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: autotrace-py
3
+ Version: 0.1.0
4
+ Summary: AutoTrace Python SDK for autonomous error tracking and AI triage
5
+ Author-email: AutoTrace Team <maintainers@autotrace.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://autotrace.io
8
+ Project-URL: Repository, https://github.com/autotrace/autotrace-py
9
+ Project-URL: Documentation, https://docs.autotrace.io
10
+ Keywords: error-tracking,ai-triage,gemini,monitoring,exceptions
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: System :: Monitoring
21
+ Classifier: Topic :: Software Development :: Bug Tracking
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ Provides-Extra: django
25
+ Requires-Dist: django>=3.2; extra == "django"
26
+ Provides-Extra: fastapi
27
+ Requires-Dist: starlette>=0.14.0; extra == "fastapi"
28
+
29
+ # AutoTrace Python SDK (`autotrace-py`)
30
+
31
+ Lightweight, zero-dependency Python client for autonomous error tracking and AI triage with [AutoTrace](https://autotrace.io).
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install autotrace
37
+ # or from source:
38
+ pip install -e ./sdk/python
39
+ ```
40
+
41
+ ## Quickstart
42
+
43
+ ```python
44
+ import autotrace
45
+
46
+ # 1. Initialize once at application startup
47
+ autotrace.init(
48
+ api_key="<YOUR_API_KEY>",
49
+ endpoint_url="http://localhost:8000/api/ingest/",
50
+ environment="production",
51
+ context={"service": "payment-api", "version": "2.1.0"},
52
+ )
53
+
54
+ # 2. Manual Exception Capture
55
+ try:
56
+ process_order()
57
+ except Exception as exc:
58
+ autotrace.capture_exception(
59
+ exc_info=exc,
60
+ endpoint="/api/v1/orders",
61
+ context={"user_id": "usr_123"},
62
+ )
63
+ ```
64
+
65
+ ## Function Decorator
66
+
67
+ ```python
68
+ @autotrace.trace(endpoint="/jobs/sync-inventory")
69
+ def sync_inventory():
70
+ # If this raises, AutoTrace captures the error and traceback
71
+ perform_risky_task()
72
+ ```
73
+
74
+ ## Framework Integrations
75
+
76
+ ### Django
77
+
78
+ In your `settings.py`:
79
+ ```python
80
+ AUTOTRACE_API_KEY = "<YOUR_API_KEY>"
81
+ AUTOTRACE_ENDPOINT = "http://localhost:8000/api/ingest/"
82
+
83
+ MIDDLEWARE = [
84
+ # ... other middleware
85
+ "autotrace.integrations.django.AutoTraceMiddleware",
86
+ ]
87
+ ```
88
+
89
+ ### FastAPI / Starlette
90
+
91
+ In your `main.py`:
92
+ ```python
93
+ from fastapi import FastAPI
94
+ import autotrace
95
+ from autotrace.integrations.fastapi import AutoTraceMiddleware
96
+
97
+ autotrace.init(api_key="<YOUR_API_KEY>")
98
+
99
+ app = FastAPI()
100
+ app.add_middleware(AutoTraceMiddleware)
101
+ ```
@@ -0,0 +1,20 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ ./autotrace/__init__.py
5
+ ./autotrace/client.py
6
+ ./autotrace/middleware.py
7
+ ./autotrace/integrations/__init__.py
8
+ ./autotrace/integrations/django.py
9
+ ./autotrace/integrations/fastapi.py
10
+ autotrace/__init__.py
11
+ autotrace/client.py
12
+ autotrace/middleware.py
13
+ autotrace/integrations/__init__.py
14
+ autotrace/integrations/django.py
15
+ autotrace/integrations/fastapi.py
16
+ autotrace_py.egg-info/PKG-INFO
17
+ autotrace_py.egg-info/SOURCES.txt
18
+ autotrace_py.egg-info/dependency_links.txt
19
+ autotrace_py.egg-info/requires.txt
20
+ autotrace_py.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+
2
+ [django]
3
+ django>=3.2
4
+
5
+ [fastapi]
6
+ starlette>=0.14.0
@@ -0,0 +1 @@
1
+ autotrace
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "autotrace-py"
7
+ version = "0.1.0"
8
+ description = "AutoTrace Python SDK for autonomous error tracking and AI triage"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "AutoTrace Team", email = "maintainers@autotrace.io"}
14
+ ]
15
+ keywords = ["error-tracking", "ai-triage", "gemini", "monitoring", "exceptions"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: System :: Monitoring",
27
+ "Topic :: Software Development :: Bug Tracking",
28
+ ]
29
+ dependencies = []
30
+
31
+ [project.optional-dependencies]
32
+ django = ["django>=3.2"]
33
+ fastapi = ["starlette>=0.14.0"]
34
+
35
+ [project.urls]
36
+ Homepage = "https://autotrace.io"
37
+ Repository = "https://github.com/autotrace/autotrace-py"
38
+ Documentation = "https://docs.autotrace.io"
39
+
40
+ [tool.setuptools]
41
+ package-dir = {"" = "."}
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["."]
45
+ include = ["autotrace*"]
46
+ namespaces = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+