Skip to content

Commit 76eadda

Browse files
add error boundary (#72)
* add error boundary * make exception logging throw away * move mockserver setup to a class method * add test for keyboard interrupts * Address Feedback
1 parent 6bfaf3a commit 76eadda

10 files changed

+555
-99
lines changed

statsig/statsig_error_boundary.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from copyreg import constructor
2+
import traceback
3+
import requests
4+
from statsig.statsig_errors import StatsigNameError, StatsigRuntimeError, StatsigValueError
5+
6+
from statsig.statsig_metadata import _StatsigMetadata
7+
8+
9+
class _StatsigErrorBoundary:
10+
endpoint = "https://statsigapi.net/v1/sdk_exception"
11+
_seen: set
12+
_api_key: str
13+
14+
def __init__(self):
15+
self._seen = set()
16+
17+
def set_api_key(self, api_key):
18+
self._api_key = api_key
19+
20+
def capture(self, task, recover=None):
21+
try:
22+
return task()
23+
except (StatsigValueError, StatsigNameError, StatsigRuntimeError) as e:
24+
raise e
25+
except Exception as e:
26+
print("[Statsig]: An unexpected exception occurred.")
27+
traceback.print_exc()
28+
29+
self.log_exception(e)
30+
31+
if (recover is None):
32+
return None
33+
return recover()
34+
35+
def log_exception(self, exception: Exception):
36+
try:
37+
name = type(exception).__name__
38+
if (self._api_key is None or name in self._seen):
39+
return
40+
41+
self._seen.add(name)
42+
meta = _StatsigMetadata.get()
43+
requests.post(self.endpoint, json={
44+
"exception": type(exception).__name__,
45+
"info": traceback.format_exc(),
46+
"statsigMetadata": _StatsigMetadata.get()
47+
}, headers={
48+
'Content-type': 'application/json',
49+
'STATSIG-API-KEY': self._api_key,
50+
'STATSIG-SDK-TYPE': meta["sdkType"],
51+
'STATSIG-SDK-VERSION': meta["sdkVersion"]
52+
})
53+
except:
54+
pass

statsig/statsig_errors.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class StatsigValueError(ValueError):
2+
pass
3+
4+
5+
class StatsigRuntimeError(RuntimeError):
6+
pass
7+
8+
9+
class StatsigNameError(NameError):
10+
pass

statsig/statsig_event.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Union, Optional
33

44
from dataclasses import dataclass
5+
from statsig.statsig_errors import StatsigValueError
56
from statsig.statsig_user import StatsigUser
67

78

@@ -21,11 +22,13 @@ class StatsigEvent:
2122

2223
def __post_init__(self):
2324
if self.user is None or not isinstance(self.user, StatsigUser):
24-
raise ValueError('StatsigEvent.user must be set')
25+
raise StatsigValueError('StatsigEvent.user must be set')
2526
if self.event_name is None or self.event_name == "":
26-
raise ValueError('StatsigEvent.event_name must be a valid str')
27+
raise StatsigValueError(
28+
'StatsigEvent.event_name must be a valid str')
2729
if self.value is not None and not isinstance(self.value, str) and not isinstance(self.value, float) and not isinstance(self.value, int):
28-
raise ValueError('StatsigEvent.value must be a str, float, or int')
30+
raise StatsigValueError(
31+
'StatsigEvent.value must be a str, float, or int')
2932

3033
def to_dict(self):
3134
evt_nullable = {

statsig/statsig_metadata.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .version import __version__
2+
3+
4+
class _StatsigMetadata:
5+
@staticmethod
6+
def get():
7+
return {
8+
"sdkVersion": __version__,
9+
"sdkType": "py-server"
10+
}

statsig/statsig_options.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from statsig.statsig_errors import StatsigValueError
12
from .statsig_environment_tier import StatsigEnvironmentTier
23
import typing
34

@@ -12,7 +13,7 @@ def __init__(
1213
timeout: typing.Optional[int] = None,
1314
rulesets_sync_interval: int = 10,
1415
idlists_sync_interval: int = 60,
15-
local_mode: bool=False,
16+
local_mode: bool = False,
1617
bootstrap_values: typing.Optional[str] = None,
1718
rules_updated_callback: typing.Optional[typing.Callable] = None,
1819
):
@@ -23,7 +24,7 @@ def __init__(
2324
tier, StatsigEnvironmentTier) else tier
2425
self.set_environment_parameter("tier", tier_str)
2526
else:
26-
raise ValueError(
27+
raise StatsigValueError(
2728
'StatsigOptions.tier must be a str or StatsigEnvironmentTier')
2829
if api is None:
2930
api = "https://statsigapi.net/v1/"
@@ -34,7 +35,7 @@ def __init__(
3435
self.local_mode = local_mode
3536
self.bootstrap_values = bootstrap_values
3637
self.rules_updated_callback = rules_updated_callback
37-
38+
3839
def set_environment_parameter(self, key: str, value: str):
3940
if self._environment is None:
4041
self._environment = {}

0 commit comments

Comments
 (0)