tinymonpy 0.1.0__tar.gz → 0.2.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.2.0}/PKG-INFO +1 -1
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/pyproject.toml +1 -1
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy/__init__.py +8 -1
- tinymonpy-0.2.0/tinymonpy/client.py +89 -0
- tinymonpy-0.2.0/tinymonpy/scrub.py +67 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy.egg-info/PKG-INFO +1 -1
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy.egg-info/SOURCES.txt +1 -0
- tinymonpy-0.1.0/tinymonpy/client.py +0 -75
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/setup.cfg +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tests/test_contract.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy/event_builder.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy/integrations.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy/scope.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy/stacktrace.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy/transport.py +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy.egg-info/dependency_links.txt +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy.egg-info/requires.txt +0 -0
- {tinymonpy-0.1.0 → tinymonpy-0.2.0}/tinymonpy.egg-info/top_level.txt +0 -0
|
@@ -34,7 +34,7 @@ __all__ = [
|
|
|
34
34
|
"_parse_traceback",
|
|
35
35
|
]
|
|
36
36
|
|
|
37
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.2.0"
|
|
38
38
|
|
|
39
39
|
_client: Optional[Client] = None
|
|
40
40
|
|
|
@@ -47,6 +47,10 @@ def init(
|
|
|
47
47
|
release: Optional[str] = None,
|
|
48
48
|
sample_rate: float = 1.0,
|
|
49
49
|
before_send: Optional[Callable[[Dict[str, Any]], Optional[Dict[str, Any]]]] = None,
|
|
50
|
+
# Privacy controls. See https://tinymon.dev/docs/privacy.html
|
|
51
|
+
scrub_url_query: bool = True,
|
|
52
|
+
scrub_patterns: Optional[list] = None,
|
|
53
|
+
send_default_pii: bool = False,
|
|
50
54
|
) -> None:
|
|
51
55
|
global _client
|
|
52
56
|
_client = Client(
|
|
@@ -56,6 +60,9 @@ def init(
|
|
|
56
60
|
release=release,
|
|
57
61
|
sample_rate=sample_rate,
|
|
58
62
|
before_send=before_send,
|
|
63
|
+
scrub_url_query=scrub_url_query,
|
|
64
|
+
scrub_patterns=scrub_patterns,
|
|
65
|
+
send_default_pii=send_default_pii,
|
|
59
66
|
)
|
|
60
67
|
install_global_handlers(_client)
|
|
61
68
|
|
|
@@ -0,0 +1,89 @@
|
|
|
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(self, exc: BaseException) -> Optional[Dict[str, Any]]:
|
|
44
|
+
snap = scope.snapshot()
|
|
45
|
+
event = build_event(
|
|
46
|
+
exc,
|
|
47
|
+
release=self.release,
|
|
48
|
+
environment=self.environment,
|
|
49
|
+
user=snap["user"],
|
|
50
|
+
tags=snap["tags"],
|
|
51
|
+
breadcrumbs=snap["breadcrumbs"],
|
|
52
|
+
)
|
|
53
|
+
# Default scrub BEFORE before_send.
|
|
54
|
+
scrub_event(
|
|
55
|
+
event,
|
|
56
|
+
scrub_url_query=self.scrub_url_query,
|
|
57
|
+
scrub_patterns=self.scrub_patterns,
|
|
58
|
+
)
|
|
59
|
+
# PII gate on the wire — server only attaches IP if true.
|
|
60
|
+
event["send_default_pii"] = bool(self.send_default_pii)
|
|
61
|
+
if self.before_send is not None:
|
|
62
|
+
result = self.before_send(event)
|
|
63
|
+
if result is None:
|
|
64
|
+
return None
|
|
65
|
+
event = result
|
|
66
|
+
return event
|
|
67
|
+
|
|
68
|
+
def capture_exception(self, exc: BaseException) -> None:
|
|
69
|
+
try:
|
|
70
|
+
if random.random() > self.sample_rate:
|
|
71
|
+
return
|
|
72
|
+
event = self._prepare(exc)
|
|
73
|
+
if event is None:
|
|
74
|
+
return
|
|
75
|
+
self.transport.enqueue(event)
|
|
76
|
+
except Exception:
|
|
77
|
+
# SWALLOW. The SDK must never throw into the host app.
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def capture_message(self, message: str, level: str = "info") -> None:
|
|
81
|
+
try:
|
|
82
|
+
event = self._prepare(Exception(message))
|
|
83
|
+
if event is None:
|
|
84
|
+
return
|
|
85
|
+
event["level"] = level
|
|
86
|
+
event["exception"]["type"] = "Message"
|
|
87
|
+
self.transport.enqueue(event)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|