tinymonpy 0.1.0__tar.gz → 0.3.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.
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/PKG-INFO +1 -1
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/pyproject.toml +1 -1
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy/__init__.py +24 -1
- tinymonpy-0.3.0/tinymonpy/client.py +118 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy/event_builder.py +8 -1
- tinymonpy-0.3.0/tinymonpy/frameworks/__init__.py +3 -0
- tinymonpy-0.3.0/tinymonpy/frameworks/asgi.py +73 -0
- tinymonpy-0.3.0/tinymonpy/frameworks/wsgi.py +57 -0
- tinymonpy-0.3.0/tinymonpy/scrub.py +67 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy.egg-info/PKG-INFO +1 -1
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy.egg-info/SOURCES.txt +5 -1
- tinymonpy-0.1.0/tinymonpy/client.py +0 -75
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/setup.cfg +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tests/test_contract.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy/integrations.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy/scope.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy/stacktrace.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy/transport.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy.egg-info/dependency_links.txt +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy.egg-info/requires.txt +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.3.0}/tinymonpy.egg-info/top_level.txt +0 -0
|
@@ -26,6 +26,7 @@ from .stacktrace import parse_traceback as _parse_traceback # public for contra
|
|
|
26
26
|
__all__ = [
|
|
27
27
|
"init",
|
|
28
28
|
"capture_exception",
|
|
29
|
+
"capture_exception_with_context",
|
|
29
30
|
"capture_message",
|
|
30
31
|
"set_user",
|
|
31
32
|
"set_tag",
|
|
@@ -34,7 +35,7 @@ __all__ = [
|
|
|
34
35
|
"_parse_traceback",
|
|
35
36
|
]
|
|
36
37
|
|
|
37
|
-
__version__ = "0.
|
|
38
|
+
__version__ = "0.3.0"
|
|
38
39
|
|
|
39
40
|
_client: Optional[Client] = None
|
|
40
41
|
|
|
@@ -47,6 +48,10 @@ def init(
|
|
|
47
48
|
release: Optional[str] = None,
|
|
48
49
|
sample_rate: float = 1.0,
|
|
49
50
|
before_send: Optional[Callable[[Dict[str, Any]], Optional[Dict[str, Any]]]] = None,
|
|
51
|
+
# Privacy controls. See https://tinymon.dev/docs/privacy.html
|
|
52
|
+
scrub_url_query: bool = True,
|
|
53
|
+
scrub_patterns: Optional[list] = None,
|
|
54
|
+
send_default_pii: bool = False,
|
|
50
55
|
) -> None:
|
|
51
56
|
global _client
|
|
52
57
|
_client = Client(
|
|
@@ -56,6 +61,9 @@ def init(
|
|
|
56
61
|
release=release,
|
|
57
62
|
sample_rate=sample_rate,
|
|
58
63
|
before_send=before_send,
|
|
64
|
+
scrub_url_query=scrub_url_query,
|
|
65
|
+
scrub_patterns=scrub_patterns,
|
|
66
|
+
send_default_pii=send_default_pii,
|
|
59
67
|
)
|
|
60
68
|
install_global_handlers(_client)
|
|
61
69
|
|
|
@@ -65,6 +73,21 @@ def capture_exception(exc: BaseException) -> None:
|
|
|
65
73
|
_client.capture_exception(exc)
|
|
66
74
|
|
|
67
75
|
|
|
76
|
+
def capture_exception_with_context(
|
|
77
|
+
exc: BaseException,
|
|
78
|
+
*,
|
|
79
|
+
request: Optional[Dict[str, Any]] = None,
|
|
80
|
+
user: Optional[Dict[str, Any]] = None,
|
|
81
|
+
tags: Optional[Dict[str, str]] = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Per-event context capture. Use from middleware; do NOT use set_user/
|
|
84
|
+
set_tag for per-request data — they race under concurrency."""
|
|
85
|
+
if _client is not None:
|
|
86
|
+
_client.capture_exception_with_context(
|
|
87
|
+
exc, request=request, user=user, tags=tags
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
68
91
|
def capture_message(msg: str, level: str = "info") -> None:
|
|
69
92
|
if _client is not None:
|
|
70
93
|
_client.capture_message(msg, level)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""The Client orchestrates capture: build event, scrub, run before_send, enqueue."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Pattern
|
|
6
|
+
|
|
7
|
+
from .event_builder import build_event
|
|
8
|
+
from .scope import scope
|
|
9
|
+
from .scrub import scrub_event
|
|
10
|
+
from .transport import Transport
|
|
11
|
+
|
|
12
|
+
_DEFAULT_ENDPOINT = "https://console.tinymon.dev/api/ingest"
|
|
13
|
+
|
|
14
|
+
BeforeSend = Callable[[Dict[str, Any]], Optional[Dict[str, Any]]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Client:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
dsn: str,
|
|
21
|
+
*,
|
|
22
|
+
endpoint: Optional[str] = None,
|
|
23
|
+
environment: Optional[str] = None,
|
|
24
|
+
release: Optional[str] = None,
|
|
25
|
+
sample_rate: float = 1.0,
|
|
26
|
+
before_send: Optional[BeforeSend] = None,
|
|
27
|
+
# Privacy controls — defaults are privacy-preserving.
|
|
28
|
+
scrub_url_query: bool = True,
|
|
29
|
+
scrub_patterns: Optional[List[Pattern[str]]] = None,
|
|
30
|
+
send_default_pii: bool = False,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.dsn = dsn
|
|
33
|
+
self.endpoint = endpoint or _DEFAULT_ENDPOINT
|
|
34
|
+
self.environment = environment
|
|
35
|
+
self.release = release
|
|
36
|
+
self.sample_rate = sample_rate
|
|
37
|
+
self.before_send = before_send
|
|
38
|
+
self.scrub_url_query = scrub_url_query
|
|
39
|
+
self.scrub_patterns = scrub_patterns
|
|
40
|
+
self.send_default_pii = send_default_pii
|
|
41
|
+
self.transport = Transport(self.endpoint, dsn)
|
|
42
|
+
|
|
43
|
+
def _prepare(
|
|
44
|
+
self,
|
|
45
|
+
exc: BaseException,
|
|
46
|
+
ctx: Optional[Dict[str, Any]] = None,
|
|
47
|
+
) -> Optional[Dict[str, Any]]:
|
|
48
|
+
snap = scope.snapshot()
|
|
49
|
+
ctx = ctx or {}
|
|
50
|
+
event = build_event(
|
|
51
|
+
exc,
|
|
52
|
+
release=self.release,
|
|
53
|
+
environment=self.environment,
|
|
54
|
+
user=ctx.get("user") or snap["user"],
|
|
55
|
+
tags=ctx.get("tags") or snap["tags"],
|
|
56
|
+
breadcrumbs=snap["breadcrumbs"],
|
|
57
|
+
request=ctx.get("request"),
|
|
58
|
+
)
|
|
59
|
+
# Default scrub BEFORE before_send.
|
|
60
|
+
scrub_event(
|
|
61
|
+
event,
|
|
62
|
+
scrub_url_query=self.scrub_url_query,
|
|
63
|
+
scrub_patterns=self.scrub_patterns,
|
|
64
|
+
)
|
|
65
|
+
# PII gate on the wire — server only attaches IP if true.
|
|
66
|
+
event["send_default_pii"] = bool(self.send_default_pii)
|
|
67
|
+
if self.before_send is not None:
|
|
68
|
+
result = self.before_send(event)
|
|
69
|
+
if result is None:
|
|
70
|
+
return None
|
|
71
|
+
event = result
|
|
72
|
+
return event
|
|
73
|
+
|
|
74
|
+
def capture_exception(self, exc: BaseException) -> None:
|
|
75
|
+
try:
|
|
76
|
+
if random.random() > self.sample_rate:
|
|
77
|
+
return
|
|
78
|
+
event = self._prepare(exc)
|
|
79
|
+
if event is None:
|
|
80
|
+
return
|
|
81
|
+
self.transport.enqueue(event)
|
|
82
|
+
except Exception:
|
|
83
|
+
# SWALLOW. The SDK must never throw into the host app.
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def capture_exception_with_context(
|
|
87
|
+
self,
|
|
88
|
+
exc: BaseException,
|
|
89
|
+
*,
|
|
90
|
+
request: Optional[Dict[str, Any]] = None,
|
|
91
|
+
user: Optional[Dict[str, Any]] = None,
|
|
92
|
+
tags: Optional[Dict[str, str]] = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Capture with per-event context. Framework middleware uses this so
|
|
95
|
+
request data is attached to THIS event without touching global scope —
|
|
96
|
+
safe under concurrent requests."""
|
|
97
|
+
try:
|
|
98
|
+
if random.random() > self.sample_rate:
|
|
99
|
+
return
|
|
100
|
+
event = self._prepare(
|
|
101
|
+
exc, {"request": request, "user": user, "tags": tags}
|
|
102
|
+
)
|
|
103
|
+
if event is None:
|
|
104
|
+
return
|
|
105
|
+
self.transport.enqueue(event)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def capture_message(self, message: str, level: str = "info") -> None:
|
|
110
|
+
try:
|
|
111
|
+
event = self._prepare(Exception(message))
|
|
112
|
+
if event is None:
|
|
113
|
+
return
|
|
114
|
+
event["level"] = level
|
|
115
|
+
event["exception"]["type"] = "Message"
|
|
116
|
+
self.transport.enqueue(event)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
@@ -28,6 +28,7 @@ def build_event(
|
|
|
28
28
|
tags: Optional[Dict[str, str]] = None,
|
|
29
29
|
breadcrumbs: Optional[List[Dict[str, Any]]] = None,
|
|
30
30
|
url: Optional[str] = None,
|
|
31
|
+
request: Optional[Dict[str, Any]] = None,
|
|
31
32
|
) -> Dict[str, Any]:
|
|
32
33
|
event: Dict[str, Any] = {
|
|
33
34
|
"event_id": str(_uuid.uuid4()),
|
|
@@ -48,7 +49,13 @@ def build_event(
|
|
|
48
49
|
event["release"] = release
|
|
49
50
|
if environment is not None:
|
|
50
51
|
event["environment"] = environment
|
|
51
|
-
|
|
52
|
+
# `request` (full dict with method+url) takes precedence over the legacy
|
|
53
|
+
# `url` kwarg used by the browser SDK.
|
|
54
|
+
if request:
|
|
55
|
+
event["request"] = {
|
|
56
|
+
k: v for k, v in request.items() if k in ("method", "url") and v
|
|
57
|
+
}
|
|
58
|
+
elif url is not None:
|
|
52
59
|
event["request"] = {"url": url}
|
|
53
60
|
return event
|
|
54
61
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""ASGI middleware. Wraps any ASGI app (FastAPI, Starlette, Django ASGI).
|
|
2
|
+
|
|
3
|
+
Usage with FastAPI::
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
import tinymonpy
|
|
7
|
+
from tinymonpy.frameworks.asgi import TinymonASGI
|
|
8
|
+
|
|
9
|
+
tinymonpy.init(dsn=os.environ["TINYMON_DSN"])
|
|
10
|
+
app = FastAPI()
|
|
11
|
+
app.add_middleware(TinymonASGI)
|
|
12
|
+
|
|
13
|
+
Captures with per-event request context (method + URL from the scope) and
|
|
14
|
+
re-raises so the framework's exception handlers still run.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Awaitable, Callable, Iterable
|
|
19
|
+
|
|
20
|
+
import tinymonpy as _tm
|
|
21
|
+
|
|
22
|
+
Scope = dict
|
|
23
|
+
Receive = Callable[[], Awaitable[dict]]
|
|
24
|
+
Send = Callable[[dict], Awaitable[None]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _url_from_scope(scope: Scope) -> str:
|
|
28
|
+
scheme = scope.get("scheme", "http")
|
|
29
|
+
headers: Iterable[tuple] = scope.get("headers") or []
|
|
30
|
+
host = ""
|
|
31
|
+
for k, v in headers:
|
|
32
|
+
if k == b"host":
|
|
33
|
+
host = v.decode("latin-1", errors="replace")
|
|
34
|
+
break
|
|
35
|
+
if not host:
|
|
36
|
+
srv = scope.get("server") or ()
|
|
37
|
+
host = f"{srv[0]}:{srv[1]}" if len(srv) >= 2 else ""
|
|
38
|
+
path = scope.get("raw_path") or scope.get("path", "")
|
|
39
|
+
if isinstance(path, bytes):
|
|
40
|
+
path = path.decode("latin-1", errors="replace")
|
|
41
|
+
qs = scope.get("query_string") or b""
|
|
42
|
+
if isinstance(qs, bytes):
|
|
43
|
+
qs = qs.decode("latin-1", errors="replace")
|
|
44
|
+
full = f"{path}?{qs}" if qs else path
|
|
45
|
+
return f"{scheme}://{host}{full}" if host else full
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TinymonASGI:
|
|
49
|
+
"""Wrap an ASGI app; capture exceptions, then re-raise."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, app: Callable[[Scope, Receive, Send], Awaitable[None]]) -> None:
|
|
52
|
+
self.app = app
|
|
53
|
+
|
|
54
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
55
|
+
if scope.get("type") not in ("http", "websocket"):
|
|
56
|
+
await self.app(scope, receive, send)
|
|
57
|
+
return
|
|
58
|
+
try:
|
|
59
|
+
await self.app(scope, receive, send)
|
|
60
|
+
except BaseException as exc:
|
|
61
|
+
client = _tm._client # type: ignore[attr-defined]
|
|
62
|
+
if client is not None:
|
|
63
|
+
try:
|
|
64
|
+
client.capture_exception_with_context(
|
|
65
|
+
exc,
|
|
66
|
+
request={
|
|
67
|
+
"method": scope.get("method"),
|
|
68
|
+
"url": _url_from_scope(scope),
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
raise
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""WSGI middleware. Wraps any WSGI app (Flask, Django sync, plain WSGI).
|
|
2
|
+
|
|
3
|
+
Usage with Flask::
|
|
4
|
+
|
|
5
|
+
from flask import Flask
|
|
6
|
+
import tinymonpy
|
|
7
|
+
from tinymonpy.frameworks.wsgi import TinymonWSGI
|
|
8
|
+
|
|
9
|
+
tinymonpy.init(dsn=os.environ["TINYMON_DSN"])
|
|
10
|
+
app = Flask(__name__)
|
|
11
|
+
app.wsgi_app = TinymonWSGI(app.wsgi_app)
|
|
12
|
+
|
|
13
|
+
The middleware captures with per-event request context (method + URL from
|
|
14
|
+
environ) and RE-RAISES — your framework's normal error handling still fires.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Callable, Iterable
|
|
19
|
+
|
|
20
|
+
import tinymonpy as _tm
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _url_from_environ(environ: dict) -> str:
|
|
24
|
+
scheme = environ.get("wsgi.url_scheme", "http")
|
|
25
|
+
host = environ.get("HTTP_HOST") or environ.get("SERVER_NAME", "")
|
|
26
|
+
path = environ.get("RAW_URI") or (
|
|
27
|
+
environ.get("SCRIPT_NAME", "") + environ.get("PATH_INFO", "")
|
|
28
|
+
)
|
|
29
|
+
qs = environ.get("QUERY_STRING")
|
|
30
|
+
if qs:
|
|
31
|
+
path = f"{path}?{qs}"
|
|
32
|
+
return f"{scheme}://{host}{path}" if host else path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TinymonWSGI:
|
|
36
|
+
"""Wrap a WSGI callable; capture exceptions, then re-raise."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, app: Callable[..., Iterable[bytes]]) -> None:
|
|
39
|
+
self.app = app
|
|
40
|
+
|
|
41
|
+
def __call__(self, environ: dict, start_response: Callable[..., Any]):
|
|
42
|
+
try:
|
|
43
|
+
return self.app(environ, start_response)
|
|
44
|
+
except BaseException as exc:
|
|
45
|
+
client = _tm._client # type: ignore[attr-defined]
|
|
46
|
+
if client is not None:
|
|
47
|
+
try:
|
|
48
|
+
client.capture_exception_with_context(
|
|
49
|
+
exc,
|
|
50
|
+
request={
|
|
51
|
+
"method": environ.get("REQUEST_METHOD"),
|
|
52
|
+
"url": _url_from_environ(environ),
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
raise
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Default PII scrub. Identical semantics to the JS SDK and the server.
|
|
2
|
+
|
|
3
|
+
Runs BEFORE before_send so user hooks can both relax and tighten the defaults.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Dict, List, Optional, Pattern
|
|
9
|
+
|
|
10
|
+
SENSITIVE_TAG_KEY: Pattern[str] = re.compile(
|
|
11
|
+
r"password|token|secret|auth|card|cvv|ssn", re.IGNORECASE
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_EMAIL = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")
|
|
15
|
+
_CARD = re.compile(r"\b(?:\d[ -]?){12,15}\d\b")
|
|
16
|
+
_BEARER = re.compile(r"\b[Bb]earer\s+[A-Za-z0-9._\-]+")
|
|
17
|
+
_AUTH_HDR = re.compile(r"\bAuthorization\s*:\s*\S+")
|
|
18
|
+
|
|
19
|
+
DEFAULT_PATTERNS: List[Pattern[str]] = [_EMAIL, _CARD, _BEARER, _AUTH_HDR]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _scrub_string(s: str, patterns: List[Pattern[str]]) -> str:
|
|
23
|
+
out = s
|
|
24
|
+
for re_ in patterns:
|
|
25
|
+
out = re_.sub("[redacted]", out)
|
|
26
|
+
return out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _strip_url_query(url: Optional[str]) -> Optional[str]:
|
|
30
|
+
if not url:
|
|
31
|
+
return url
|
|
32
|
+
for i, ch in enumerate(url):
|
|
33
|
+
if ch in "?#":
|
|
34
|
+
return url[:i]
|
|
35
|
+
return url
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def scrub_event(
|
|
39
|
+
event: Dict[str, Any],
|
|
40
|
+
*,
|
|
41
|
+
scrub_url_query: bool = True,
|
|
42
|
+
scrub_patterns: Optional[List[Pattern[str]]] = None,
|
|
43
|
+
) -> Dict[str, Any]:
|
|
44
|
+
"""Mutate the event dict in place. Returns it for chaining."""
|
|
45
|
+
patterns = list(DEFAULT_PATTERNS) + list(scrub_patterns or [])
|
|
46
|
+
|
|
47
|
+
req = event.get("request") or {}
|
|
48
|
+
if scrub_url_query and req.get("url"):
|
|
49
|
+
req["url"] = _strip_url_query(req.get("url"))
|
|
50
|
+
event["request"] = req
|
|
51
|
+
|
|
52
|
+
exc = event.get("exception") or {}
|
|
53
|
+
if exc.get("value"):
|
|
54
|
+
exc["value"] = _scrub_string(exc["value"], patterns)
|
|
55
|
+
|
|
56
|
+
for b in event.get("breadcrumbs") or []:
|
|
57
|
+
if b.get("message"):
|
|
58
|
+
b["message"] = _scrub_string(b["message"], patterns)
|
|
59
|
+
|
|
60
|
+
tags = event.get("tags")
|
|
61
|
+
if tags:
|
|
62
|
+
cleaned: Dict[str, str] = {}
|
|
63
|
+
for k, v in tags.items():
|
|
64
|
+
cleaned[k] = "[redacted]" if SENSITIVE_TAG_KEY.search(k) else v
|
|
65
|
+
event["tags"] = cleaned
|
|
66
|
+
|
|
67
|
+
return event
|
|
@@ -5,10 +5,14 @@ tinymonpy/client.py
|
|
|
5
5
|
tinymonpy/event_builder.py
|
|
6
6
|
tinymonpy/integrations.py
|
|
7
7
|
tinymonpy/scope.py
|
|
8
|
+
tinymonpy/scrub.py
|
|
8
9
|
tinymonpy/stacktrace.py
|
|
9
10
|
tinymonpy/transport.py
|
|
10
11
|
tinymonpy.egg-info/PKG-INFO
|
|
11
12
|
tinymonpy.egg-info/SOURCES.txt
|
|
12
13
|
tinymonpy.egg-info/dependency_links.txt
|
|
13
14
|
tinymonpy.egg-info/requires.txt
|
|
14
|
-
tinymonpy.egg-info/top_level.txt
|
|
15
|
+
tinymonpy.egg-info/top_level.txt
|
|
16
|
+
tinymonpy/frameworks/__init__.py
|
|
17
|
+
tinymonpy/frameworks/asgi.py
|
|
18
|
+
tinymonpy/frameworks/wsgi.py
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"""The Client orchestrates capture: build event, run beforeSend, enqueue."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import random
|
|
5
|
-
from typing import Any, Callable, Dict, Optional
|
|
6
|
-
|
|
7
|
-
from .event_builder import build_event
|
|
8
|
-
from .scope import scope
|
|
9
|
-
from .transport import Transport
|
|
10
|
-
|
|
11
|
-
_DEFAULT_ENDPOINT = "https://console.tinymon.dev/api/ingest"
|
|
12
|
-
|
|
13
|
-
BeforeSend = Callable[[Dict[str, Any]], Optional[Dict[str, Any]]]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Client:
|
|
17
|
-
def __init__(
|
|
18
|
-
self,
|
|
19
|
-
dsn: str,
|
|
20
|
-
*,
|
|
21
|
-
endpoint: Optional[str] = None,
|
|
22
|
-
environment: Optional[str] = None,
|
|
23
|
-
release: Optional[str] = None,
|
|
24
|
-
sample_rate: float = 1.0,
|
|
25
|
-
before_send: Optional[BeforeSend] = None,
|
|
26
|
-
) -> None:
|
|
27
|
-
self.dsn = dsn
|
|
28
|
-
self.endpoint = endpoint or _DEFAULT_ENDPOINT
|
|
29
|
-
self.environment = environment
|
|
30
|
-
self.release = release
|
|
31
|
-
self.sample_rate = sample_rate
|
|
32
|
-
self.before_send = before_send
|
|
33
|
-
self.transport = Transport(self.endpoint, dsn)
|
|
34
|
-
|
|
35
|
-
def capture_exception(self, exc: BaseException) -> None:
|
|
36
|
-
try:
|
|
37
|
-
if random.random() > self.sample_rate:
|
|
38
|
-
return
|
|
39
|
-
snap = scope.snapshot()
|
|
40
|
-
event = build_event(
|
|
41
|
-
exc,
|
|
42
|
-
release=self.release,
|
|
43
|
-
environment=self.environment,
|
|
44
|
-
user=snap["user"],
|
|
45
|
-
tags=snap["tags"],
|
|
46
|
-
breadcrumbs=snap["breadcrumbs"],
|
|
47
|
-
)
|
|
48
|
-
if self.before_send is not None:
|
|
49
|
-
result = self.before_send(event)
|
|
50
|
-
if result is None:
|
|
51
|
-
return
|
|
52
|
-
event = result
|
|
53
|
-
self.transport.enqueue(event)
|
|
54
|
-
except Exception:
|
|
55
|
-
# SWALLOW. The SDK must never throw into the host app.
|
|
56
|
-
pass
|
|
57
|
-
|
|
58
|
-
def capture_message(self, message: str, level: str = "info") -> None:
|
|
59
|
-
try:
|
|
60
|
-
# Synthesize an exception without a stack — value carries the message.
|
|
61
|
-
synthetic = Exception(message)
|
|
62
|
-
snap = scope.snapshot()
|
|
63
|
-
event = build_event(
|
|
64
|
-
synthetic,
|
|
65
|
-
release=self.release,
|
|
66
|
-
environment=self.environment,
|
|
67
|
-
user=snap["user"],
|
|
68
|
-
tags=snap["tags"],
|
|
69
|
-
breadcrumbs=snap["breadcrumbs"],
|
|
70
|
-
)
|
|
71
|
-
event["level"] = level
|
|
72
|
-
event["exception"]["type"] = "Message"
|
|
73
|
-
self.transport.enqueue(event)
|
|
74
|
-
except Exception:
|
|
75
|
-
pass
|
|
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
|