Skip to content

Commit 336acef

Browse files
Merge pull request #16 from statsig-io/0-16-9
v0.16.9 - Expose group_name, init diagnostics, top-level flush, & more
2 parents abefc43 + 4cda63b commit 336acef

35 files changed

+619
-97
lines changed

.github/workflows/kong.yml

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
name: KONG
22

3+
env:
4+
test_api_key: ${{ secrets.KONG_SERVER_SDK_KEY }}
5+
test_client_key: ${{ secrets. KONG_CLIENT_SDK_KEY }}
6+
repo_pat: ${{ secrets.KONG_FINE_GRAINED_REPO_PAT }}
7+
FORCE_COLOR: true
8+
39
on:
410
workflow_dispatch:
511
schedule:
@@ -14,26 +20,22 @@ jobs:
1420
timeout-minutes: 5
1521
runs-on: ubuntu-latest
1622
steps:
17-
- uses: actions/setup-python@v2
18-
with:
19-
python-version: "3.8"
20-
2123
- name: Get KONG
2224
run: |
23-
git clone https://${{ secrets.KONG_REPO_PAT }}@github.com/statsig-io/kong.git
25+
git clone https://oauth2:$repo_pat@github.com/statsig-io/kong.git .
2426
25-
- name: Setup Python Server
26-
run: |
27-
cd kong/bridges/python-server
28-
BRANCH="${GITHUB_HEAD_REF:-main}"; pip install statsig git+https://${repo_pat}@github.com/$GITHUB_REPOSITORY.git@$BRANCH
29-
env:
30-
repo_pat: ${{ secrets.KONG_REPO_PAT }}
27+
- uses: actions/setup-node@v1
28+
with:
29+
node-version: "16.x"
30+
31+
- name: Install Deps
32+
run: npm install
33+
34+
- name: Setup Python SDK
35+
run: npm run kong -- setup python -v
36+
37+
- name: Build Bridge
38+
run: npm run kong -- build python -v
3139

3240
- name: Run Tests
33-
run: |
34-
cd kong
35-
npm install
36-
FORCE_COLOR=true npm run kong -- test python -w -r
37-
env:
38-
test_api_key: ${{ secrets.KONG_SERVER_SDK_KEY }}
39-
test_client_key: ${{ secrets.KONG_CLIENT_SDK_KEY }}
41+
run: npm run kong -- test python -v -r

statsig/diagnostics.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import time
2+
3+
4+
class _Diagnostics:
5+
def __init__(self, context):
6+
self.context = context
7+
self.markers = []
8+
9+
def mark(self, key, action, step=None, value=None):
10+
marker = Marker(key, action, step, value)
11+
self.markers.append(marker)
12+
13+
def serialize(self) -> object:
14+
return {
15+
"markers": [marker.serialize() for marker in self.markers],
16+
"context": self.context
17+
}
18+
19+
20+
class Marker:
21+
def __init__(self, key, action, step, value):
22+
if key is None:
23+
key = ""
24+
self.key = key
25+
self.action = action
26+
self.step = step
27+
self.value = value
28+
self.timestamp = round(time.time()*1000)
29+
30+
def serialize(self) -> object:
31+
return {
32+
"key": self.key,
33+
"step": self.step,
34+
"action": self.action,
35+
"value": self.value,
36+
"timestamp": self.timestamp,
37+
}

statsig/dynamic_config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
class DynamicConfig:
2-
def __init__(self, data, name, rule):
2+
def __init__(self, data, name, rule, group_name=None):
33
if data is None:
44
data = {}
55
self.value = data
@@ -9,6 +9,7 @@ def __init__(self, data, name, rule):
99
if rule is None:
1010
rule = ""
1111
self.rule_id = rule
12+
self.group_name = group_name
1213

1314
def get(self, key, default=None):
1415
"""Returns the value of the config at the given key

statsig/evaluator.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ def __init__(self,
2424
allocated_experiment=None,
2525
explicit_parameters=None,
2626
is_experiment_group=False,
27-
evaluation_details=None):
27+
evaluation_details=None,
28+
group_name=None):
2829
if fetch_from_server is None:
2930
fetch_from_server = False
3031
self.fetch_from_server = fetch_from_server
@@ -46,8 +47,8 @@ def __init__(self,
4647
self.allocated_experiment = allocated_experiment
4748
self.explicit_parameters = explicit_parameters
4849
self.is_experiment_group = is_experiment_group is True
49-
5050
self.evaluation_details = evaluation_details
51+
self.group_name = group_name
5152

5253

5354
class _Evaluator:
@@ -225,7 +226,8 @@ def __evaluate(self, user, config):
225226
result.rule_id,
226227
exposures,
227228
is_experiment_group=result.is_experiment_group,
228-
evaluation_details=evaluation_details
229+
evaluation_details=evaluation_details,
230+
group_name=result.group_name
229231
)
230232

231233
return _ConfigEvaluation(False, False, default_value, "default", exposures,
@@ -247,7 +249,8 @@ def __evaluate_rule(self, user, rule):
247249
rule_id = rule.get("id", "")
248250

249251
return _ConfigEvaluation(False, eval_result, return_value, rule_id, exposures,
250-
is_experiment_group=rule.get("isExperimentGroup", False))
252+
is_experiment_group=rule.get("isExperimentGroup", False),
253+
group_name=rule.get("groupName", None))
251254

252255
def __evaluate_delegate(self, user, rule, exposures):
253256
config_delegate = rule.get('configDelegate', None)

statsig/spec_store.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
from .statsig_network import _StatsigNetwork
1010
from .statsig_options import StatsigOptions
1111
from .thread_util import spawn_background_thread, THREAD_JOIN_TIMEOUT
12+
from .diagnostics import _Diagnostics
1213
from .utils import logger
1314

1415
RULESETS_SYNC_INTERVAL = 10
1516
IDLISTS_SYNC_INTERVAL = 60
1617
STORAGE_ADAPTER_KEY = "statsig.cache"
1718
SYNC_OUTDATED_MAX_S = 120
1819

20+
1921
def _is_specs_json_valid(specs_json):
2022
if specs_json is None or specs_json.get("time") is None:
2123
return False
@@ -30,7 +32,8 @@ class _SpecStore:
3032
_background_download_id_lists: Optional[threading.Thread]
3133

3234
def __init__(self, network: _StatsigNetwork, options: StatsigOptions, statsig_metadata: dict,
33-
error_boundary: _StatsigErrorBoundary, shutdown_event: threading.Event):
35+
error_boundary: _StatsigErrorBoundary, shutdown_event: threading.Event,
36+
init_diagnostics: _Diagnostics):
3437
self.last_update_time = 0
3538
self.initial_update_time = 0
3639
self.init_reason = EvaluationReason.uninitialized
@@ -44,6 +47,7 @@ def __init__(self, network: _StatsigNetwork, options: StatsigOptions, statsig_me
4447
self._background_download_configs = None
4548
self._background_download_id_lists = None
4649
self._sync_failure_count = 0
50+
self._init_diagnostics = init_diagnostics
4751

4852
self._configs = {}
4953
self._gates = {}
@@ -119,14 +123,15 @@ def _initialize_specs(self):
119123
if self._options.bootstrap_values is not None:
120124
logger.warning(
121125
"data_store gets priority over bootstrap_values. bootstrap_values will be ignored")
122-
123126
self._load_config_specs_from_storage_adapter()
124127
if self.last_update_time == 0:
125128
self._log_process("Retrying with network...")
126129
self._download_config_specs()
127130

128131
elif self._options.bootstrap_values is not None:
132+
self._init_diagnostics.mark("bootstrap", "start", "load")
129133
self._bootstrap_config_specs()
134+
self._init_diagnostics.mark("bootstrap", "end", "load")
130135

131136
else:
132137
self._download_config_specs()
@@ -202,22 +207,28 @@ def _download_config_specs(self):
202207
log_on_exception = True
203208
self._sync_failure_count = 0
204209

205-
specs = self._network.post_request("download_config_specs", {
206-
"statsigMetadata": self._statsig_metadata,
207-
"sinceTime": self.last_update_time,
208-
}, log_on_exception)
210+
try:
211+
specs = self._network.post_request("download_config_specs", {
212+
"statsigMetadata": self._statsig_metadata,
213+
"sinceTime": self.last_update_time,
214+
}, log_on_exception, self._init_diagnostics)
215+
except Exception as e:
216+
raise e
209217

210218
if specs is None:
211219
self._sync_failure_count += 1
212220
return
213221

222+
self._init_diagnostics.mark("download_config_specs", "start", "process")
214223
if not _is_specs_json_valid(specs):
224+
self._init_diagnostics.mark("download_config_specs", "end", "process", False)
215225
return
216226

217227
self._log_process("Done loading specs")
218228
if self._process_specs(specs):
219229
self._save_to_storage_adapter(specs)
220230
self.init_reason = EvaluationReason.network
231+
self._init_diagnostics.mark("download_config_specs", "end", "process", self.init_reason == EvaluationReason.network)
221232

222233
def _save_to_storage_adapter(self, specs):
223234
if not _is_specs_json_valid(specs):
@@ -232,6 +243,7 @@ def _save_to_storage_adapter(self, specs):
232243
self._options.data_store.set(STORAGE_ADAPTER_KEY, json.dumps(specs))
233244

234245
def _load_config_specs_from_storage_adapter(self):
246+
self._init_diagnostics.mark("bootstrap", "start", "load")
235247
self._log_process("Loading specs from adapter")
236248
if self._options.data_store is None:
237249
return
@@ -254,6 +266,7 @@ def _load_config_specs_from_storage_adapter(self):
254266
self._log_process("Done loading specs")
255267
if self._process_specs(cache):
256268
self.init_reason = EvaluationReason.data_adapter
269+
self._init_diagnostics.mark("bootstrap", "end", "load")
257270

258271
def _spawn_bg_download_id_lists(self):
259272
self._background_download_id_lists = spawn_background_thread(self._sync, (
@@ -264,9 +277,11 @@ def _download_id_lists(self):
264277
try:
265278
server_id_lists = self._network.post_request("get_id_lists", {
266279
"statsigMetadata": self._statsig_metadata,
267-
})
280+
}, False, self._init_diagnostics)
281+
268282
if server_id_lists is None:
269283
return
284+
self._init_diagnostics.mark("get_id_lists", "start", "process")
270285

271286
local_id_lists = self._id_lists
272287
workers = []
@@ -321,7 +336,10 @@ def _download_id_lists(self):
321336
# remove any list that has been deleted
322337
for list_name in deleted_lists:
323338
local_id_lists.pop(list_name, None)
339+
self._init_diagnostics.mark("get_id_lists", "end", "process", True)
340+
324341
except Exception as e:
342+
self._init_diagnostics.mark("get_id_lists", "end", "process", False)
325343
self._error_boundary.log_exception(e)
326344

327345
def _download_single_id_list(

statsig/statsig.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,14 @@ def evaluate_all(user: StatsigUser):
256256
"""
257257
return __instance.evaluate_all(user)
258258

259+
def flush():
260+
"""
261+
Flushes any queued event logs
262+
NOTE: the sdk flushes events in the background every minute or 500 events
263+
For long running webservers, let the sdk manage the background flush
264+
when using the sdk in a script or scenario where you need to flush logs, use this method
265+
"""
266+
__instance.flush()
259267

260268
def shutdown():
261269
"""

statsig/statsig_event.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ class StatsigEvent:
2222
_time: int = field(default_factory=lambda: round(time.time() * 1000))
2323

2424
def __post_init__(self):
25-
if self.user is None or not isinstance(self.user, StatsigUser):
26-
raise StatsigValueError('StatsigEvent.user must be set')
2725
if self.event_name is None or self.event_name == "":
2826
raise StatsigValueError(
2927
'StatsigEvent.event_name must be a valid str')

0 commit comments

Comments
 (0)