vaka-sdk 0.2.0__py3-none-any.whl

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.
vaka/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """Vaka SDK for Python: crash reporting, release health, analytics and feature flags.
2
+
3
+ import vaka
4
+ vaka.init(key="vk_...", endpoint="https://vaka.example.com", release="2.4.0")
5
+
6
+ Unhandled exceptions (main thread and ``threading`` threads), ``logging`` errors and one session
7
+ per process are reported automatically. Traces and logs go through OpenTelemetry: see
8
+ ``vaka.otel.setup()``. Docs: https://vaka.hellsoft.se/docs/sdk-python
9
+ """
10
+
11
+ from ._api import (
12
+ add_breadcrumb,
13
+ capture_exception,
14
+ capture_message,
15
+ evaluate_flags,
16
+ flush,
17
+ get_client,
18
+ get_flag,
19
+ identify,
20
+ init,
21
+ is_enabled,
22
+ is_initialized,
23
+ load_flags,
24
+ monitor,
25
+ set_context,
26
+ set_tag,
27
+ set_tags,
28
+ set_user,
29
+ shutdown,
30
+ track,
31
+ )
32
+ from ._client import Client, Options, Scope, current_scope, isolation_scope
33
+ from ._version import __version__
34
+ from .flags import Flags
35
+
36
+ __all__ = [
37
+ "__version__",
38
+ "init",
39
+ "get_client",
40
+ "is_initialized",
41
+ "capture_exception",
42
+ "capture_message",
43
+ "add_breadcrumb",
44
+ "set_user",
45
+ "identify",
46
+ "set_tag",
47
+ "set_tags",
48
+ "set_context",
49
+ "track",
50
+ "flush",
51
+ "shutdown",
52
+ "monitor",
53
+ "load_flags",
54
+ "evaluate_flags",
55
+ "get_flag",
56
+ "is_enabled",
57
+ "isolation_scope",
58
+ "current_scope",
59
+ "Client",
60
+ "Options",
61
+ "Scope",
62
+ "Flags",
63
+ ]
vaka/_api.py ADDED
@@ -0,0 +1,254 @@
1
+ """Module-level API (``vaka.init``, ``vaka.capture_exception`` …) around one global client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import threading
7
+ from typing import Any, Callable, Dict, Mapping, Optional, TypeVar
8
+
9
+ from . import flags as _flags
10
+ from ._client import Client, Options, Scope, current_scope, isolation_scope, now_ms, MAX_BREADCRUMBS
11
+
12
+ _client: Optional[Client] = None
13
+ _lock = threading.Lock()
14
+
15
+ F = TypeVar("F", bound=Callable[..., Any])
16
+
17
+
18
+ def init(key: Optional[str] = None, endpoint: Optional[str] = None, **options: Any) -> Optional[Client]:
19
+ """Start the SDK. Call once, as early as possible.
20
+
21
+ ``key`` and ``endpoint`` default to ``$VAKA_KEY`` and ``$VAKA_ENDPOINT``; without both the SDK
22
+ stays disabled and every call is a no-op. See ``vaka.Options`` for the other settings.
23
+ Calling ``init`` again replaces the previous client (it is flushed and closed first).
24
+ """
25
+ global _client
26
+ opts = Options(key=key, endpoint=endpoint, **options)
27
+ if not opts.key or not opts.endpoint:
28
+ if opts.debug or key or endpoint:
29
+ print("[vaka] init: `key` and `endpoint` are required; SDK disabled", file=sys.stderr)
30
+ return None
31
+ with _lock:
32
+ old, _client = _client, None
33
+ if old is not None:
34
+ old.close(timeout=2.0)
35
+ client = Client(opts)
36
+ with _lock:
37
+ _client = client
38
+ client.install_hooks()
39
+ if opts.sessions:
40
+ client.start_session()
41
+ if opts.flags:
42
+ threading.Thread(target=_load_flags_quietly, name="vaka-flags", daemon=True).start()
43
+ client.log(f"initialized (release={opts.release}, environment={opts.environment})")
44
+ return client
45
+
46
+
47
+ def get_client() -> Optional[Client]:
48
+ return _client
49
+
50
+
51
+ def is_initialized() -> bool:
52
+ return _client is not None
53
+
54
+
55
+ def capture_exception(exc: Optional[BaseException] = None, **kwargs: Any) -> Optional[str]:
56
+ """Report an exception (default: the one being handled). Returns the event id.
57
+
58
+ Keyword arguments: ``level`` ("error"), ``tags``, ``contexts``, ``fingerprint``, ``handled``.
59
+ """
60
+ c = _client
61
+ return c.capture_exception(exc, **kwargs) if c else None
62
+
63
+
64
+ def capture_message(message: str, level: str = "info", **kwargs: Any) -> Optional[str]:
65
+ """Report a message with the current stack. Returns the event id."""
66
+ c = _client
67
+ return c.capture_message(message, level=level, **kwargs) if c else None
68
+
69
+
70
+ def add_breadcrumb(
71
+ message: str,
72
+ category: str = "default",
73
+ level: str = "info",
74
+ data: Optional[Mapping[str, Any]] = None,
75
+ type: Optional[str] = None,
76
+ ) -> None:
77
+ """Record a breadcrumb; the last 100 are sent with the next error.
78
+
79
+ Categories with special meaning: ``navigation`` (``data: {from, to}``), ``http`` (``data:
80
+ {method, url, status_code, duration_ms}``), ``console``, ``lifecycle``.
81
+ """
82
+ if _client is None:
83
+ return
84
+ crumb: Dict[str, Any] = {"timestamp": now_ms(), "category": category, "message": message, "level": level}
85
+ if data:
86
+ crumb["data"] = dict(data)
87
+ if type:
88
+ crumb["type"] = type
89
+ crumbs = current_scope().breadcrumbs
90
+ crumbs.append(crumb)
91
+ if len(crumbs) > MAX_BREADCRUMBS:
92
+ del crumbs[: len(crumbs) - MAX_BREADCRUMBS]
93
+
94
+
95
+ def set_user(user_id: Optional[str]) -> None:
96
+ """Set (or clear with ``None``) the identified user. Opt-in: only if your privacy policy allows."""
97
+ current_scope().user_id = str(user_id) if user_id is not None else None
98
+
99
+
100
+ identify = set_user
101
+
102
+
103
+ def set_tag(key: str, value: Any) -> None:
104
+ """Set a tag sent with every error from the current scope."""
105
+ current_scope().tags[key] = str(value)
106
+
107
+
108
+ def set_tags(tags: Mapping[str, Any]) -> None:
109
+ for k, v in tags.items():
110
+ set_tag(k, v)
111
+
112
+
113
+ def set_context(name: str, value: Mapping[str, Any]) -> None:
114
+ """Attach structured context (shown on errors under ``contexts.<name>``)."""
115
+ current_scope().contexts[name] = dict(value)
116
+
117
+
118
+ def track(name: str, props: Optional[Mapping[str, Any]] = None, **fields: Any) -> None:
119
+ """Track a custom analytics event. ``props``: flat str/number/bool values, at most 64."""
120
+ c = _client
121
+ if c is not None:
122
+ c.track(name, props, **fields)
123
+
124
+
125
+ def flush(timeout: float = 2.0) -> bool:
126
+ """Send everything queued, waiting at most ``timeout`` seconds."""
127
+ c = _client
128
+ return c.flush(timeout) if c else True
129
+
130
+
131
+ def shutdown(timeout: float = 5.0) -> None:
132
+ """End the session, flush and stop the SDK (also runs automatically at exit)."""
133
+ global _client
134
+ with _lock:
135
+ c, _client = _client, None
136
+ if c is not None:
137
+ c.close(timeout)
138
+
139
+
140
+ def monitor(fn: F) -> F:
141
+ """Decorator: report exceptions escaping ``fn`` (re-raised) inside an isolation scope.
142
+
143
+ Handy for background jobs, task-queue workers and serverless handlers; also flushes.
144
+ """
145
+ import functools
146
+ import inspect
147
+
148
+ if inspect.iscoroutinefunction(fn):
149
+
150
+ @functools.wraps(fn)
151
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
152
+ with isolation_scope():
153
+ try:
154
+ return await fn(*args, **kwargs)
155
+ except Exception as e:
156
+ capture_exception(e, handled=False, tags={"mechanism": "monitor", "function": fn.__qualname__})
157
+ flush()
158
+ raise
159
+
160
+ return async_wrapper # type: ignore[return-value]
161
+
162
+ @functools.wraps(fn)
163
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
164
+ with isolation_scope():
165
+ try:
166
+ return fn(*args, **kwargs)
167
+ except Exception as e:
168
+ capture_exception(e, handled=False, tags={"mechanism": "monitor", "function": fn.__qualname__})
169
+ flush()
170
+ raise
171
+
172
+ return wrapper # type: ignore[return-value]
173
+
174
+
175
+ # ----- feature flags ---------------------------------------------------------------------
176
+
177
+
178
+ def load_flags(
179
+ properties: Optional[Mapping[str, Any]] = None,
180
+ distinct_id: Optional[str] = None,
181
+ user_id: Optional[str] = None,
182
+ timeout: float = 3.0,
183
+ ) -> _flags.Flags:
184
+ """Evaluate all flags for this process (or ``distinct_id``) and cache them for ``get_flag``."""
185
+ c = _client
186
+ if c is None:
187
+ return _flags.Flags()
188
+ props = dict(c.options.flag_properties)
189
+ props.update(properties or {})
190
+ result = _flags.evaluate(
191
+ c.options.endpoint,
192
+ c.options.key,
193
+ distinct_id or c.distinct_id,
194
+ user_id=user_id or current_scope().user_id,
195
+ environment=c.options.environment,
196
+ properties=props,
197
+ timeout=timeout,
198
+ )
199
+ c.flags = result
200
+ return result
201
+
202
+
203
+ def _load_flags_quietly() -> None:
204
+ try:
205
+ load_flags()
206
+ except Exception as e:
207
+ c = _client
208
+ if c is not None:
209
+ c.log(f"loading flags failed: {e}")
210
+
211
+
212
+ def evaluate_flags(
213
+ distinct_id: str,
214
+ user_id: Optional[str] = None,
215
+ properties: Optional[Mapping[str, Any]] = None,
216
+ timeout: float = 3.0,
217
+ ) -> _flags.Flags:
218
+ """Evaluate flags for one person (e.g. per request on a server). Nothing is cached.
219
+
220
+ Values read with ``.get()`` are recorded on the current (request) scope.
221
+ """
222
+ c = _client
223
+ if c is None:
224
+ return _flags.Flags()
225
+ props = dict(c.options.flag_properties)
226
+ props.update(properties or {})
227
+ return _flags.evaluate(
228
+ c.options.endpoint, c.options.key, distinct_id, user_id=user_id,
229
+ environment=c.options.environment, properties=props, timeout=timeout,
230
+ )
231
+
232
+
233
+ def get_flag(key: str, default: Any = None) -> Any:
234
+ """A flag value from the last ``load_flags()`` (recorded as an exposure)."""
235
+ c = _client
236
+ if c is None or c.flags is None:
237
+ if default is not None:
238
+ _flags.expose(key, default)
239
+ return default
240
+ return c.flags.get(key, default)
241
+
242
+
243
+ def is_enabled(key: str, default: bool = False) -> bool:
244
+ c = _client
245
+ if c is None or c.flags is None:
246
+ return default
247
+ return c.flags.is_enabled(key, default)
248
+
249
+
250
+ __all__ = [
251
+ "init", "get_client", "is_initialized", "capture_exception", "capture_message", "add_breadcrumb",
252
+ "set_user", "identify", "set_tag", "set_tags", "set_context", "track", "flush", "shutdown",
253
+ "monitor", "load_flags", "evaluate_flags", "get_flag", "is_enabled", "isolation_scope", "Scope",
254
+ ]