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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinymonpy
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Tiny error monitoring SDK for Python.
5
5
  Author: tinymon
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinymonpy"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Tiny error monitoring SDK for Python."
9
9
  requires-python = ">=3.8"
10
10
  license = { text = "MIT" }
@@ -34,7 +34,7 @@ __all__ = [
34
34
  "_parse_traceback",
35
35
  ]
36
36
 
37
- __version__ = "0.1.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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinymonpy
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Tiny error monitoring SDK for Python.
5
5
  Author: tinymon
6
6
  License: MIT
@@ -5,6 +5,7 @@ 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
@@ -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