Skip to content

Commit 692c50a

Browse files
authored
sample more special cases (#340)
1 parent 77d8c9f commit 692c50a

File tree

5 files changed

+128
-65
lines changed

5 files changed

+128
-65
lines changed

statsig/spec_store.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import threading
33
from concurrent.futures import wait, ThreadPoolExecutor
4+
from enum import Enum
45
from typing import List, Optional, Dict, Set, Tuple
56

67
from . import globals
@@ -15,6 +16,12 @@
1516
from .utils import djb2_hash
1617

1718

19+
class EntityType(Enum):
20+
GATE = "feature_gates"
21+
CONFIG = "dynamic_configs"
22+
LAYER = "layer_configs"
23+
24+
1825
class _SpecStore:
1926
_background_download_configs: Optional[threading.Thread]
2027
_background_download_id_lists: Optional[threading.Thread]
@@ -202,9 +209,9 @@ def parse_target_value_map_from_spec(spec, parsed):
202209
rule["conditions"][i]["fast_target_value"][str(val)] = True
203210

204211
self.unsupported_configs.clear()
205-
new_gates = get_parsed_specs("feature_gates")
206-
new_configs = get_parsed_specs("dynamic_configs")
207-
new_layers = get_parsed_specs("layer_configs")
212+
new_gates = get_parsed_specs(EntityType.GATE.value)
213+
new_configs = get_parsed_specs(EntityType.CONFIG.value)
214+
new_layers = get_parsed_specs(EntityType.LAYER.value)
208215

209216
new_experiment_to_layer = {}
210217
layers_dict = specs_json.get("layers", {})
@@ -353,7 +360,8 @@ def _get_initialize_strategy(self) -> List[DataSource]:
353360
strategies.insert(0, DataSource.DATASTORE)
354361
if self._options.bootstrap_values:
355362
if data_store is not None:
356-
globals.logger.debug("data_store gets priority over bootstrap_values. bootstrap_values will be ignored")
363+
globals.logger.debug(
364+
"data_store gets priority over bootstrap_values. bootstrap_values will be ignored")
357365
else:
358366
strategies.insert(0, DataSource.BOOTSTRAP)
359367
if self._options.fallback_to_statsig_api:

statsig/statsig_logger.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def log_gate_exposure(
101101
if is_manual_exposure:
102102
event.metadata["isManualExposure"] = "true"
103103
if sampling_rate is not None:
104-
event.statsigMetadata = {"samplingRate": sampling_rate}
104+
event.statsigMetadata["samplingRate"] = sampling_rate
105105
if shadow_logged is not None:
106106
event.statsigMetadata["shadowLogged"] = shadow_logged
107107
if sampling_mode is not None:
@@ -138,7 +138,7 @@ def log_config_exposure(
138138
if is_manual_exposure:
139139
event.metadata["isManualExposure"] = "true"
140140
if sampling_rate is not None:
141-
event.statsigMetadata = {"samplingRate": sampling_rate}
141+
event.statsigMetadata["samplingRate"] = sampling_rate
142142
if shadow_logged is not None:
143143
event.statsigMetadata["shadowLogged"] = shadow_logged
144144
if sampling_mode is not None:
@@ -158,6 +158,9 @@ def log_layer_exposure(
158158
parameter_name: str,
159159
config_evaluation: _ConfigEvaluation,
160160
is_manual_exposure=False,
161+
sampling_rate=None,
162+
shadow_logged=None,
163+
sampling_mode=None,
161164
):
162165
event = StatsigEvent(user, _LAYER_EXPOSURE_EVENT)
163166

@@ -178,8 +181,15 @@ def log_layer_exposure(
178181
if not self._is_unique_exposure(user, _LAYER_EXPOSURE_EVENT, metadata):
179182
return
180183
event.metadata = metadata
184+
event.statsigMetadata = {}
181185
if is_manual_exposure:
182186
event.metadata["isManualExposure"] = "true"
187+
if sampling_rate is not None:
188+
event.statsigMetadata["samplingRate"] = sampling_rate
189+
if shadow_logged is not None:
190+
event.statsigMetadata["shadowLogged"] = shadow_logged
191+
if sampling_mode is not None:
192+
event.statsigMetadata["samplingMode"] = sampling_mode
183193

184194
event._secondary_exposures = [] if exposures is None else exposures
185195

statsig/statsig_server.py

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .feature_gate import FeatureGate
1111
from .layer import Layer
1212
from .sdk_configs import _SDK_Configs
13-
from .spec_store import _SpecStore
13+
from .spec_store import _SpecStore, EntityType
1414
from .statsig_error_boundary import _StatsigErrorBoundary
1515
from .statsig_errors import StatsigNameError, StatsigRuntimeError, StatsigValueError
1616
from .statsig_event import StatsigEvent
@@ -21,7 +21,7 @@
2121
from .statsig_user import StatsigUser
2222
from .ttl_set import TTLSet
2323
from .utils import HashingAlgorithm, compute_dedupe_key_for_gate, is_hash_in_sampling_rate, \
24-
compute_dedupe_key_for_config
24+
compute_dedupe_key_for_config, compute_dedupe_key_for_layer
2525

2626
RULESETS_SYNC_INTERVAL = 10
2727
IDLISTS_SYNC_INTERVAL = 60
@@ -278,9 +278,14 @@ def task():
278278
result = self._evaluator.get_layer(normal_user, layer_name)
279279

280280
def log_func(layer: Layer, parameter_name: str):
281-
if log_exposure:
281+
should_log, logged_sampling_rate, shadow_logged = self.__determine_sampling(
282+
EntityType.LAYER, layer_name, result, user, parameter_name)
283+
284+
if log_exposure and should_log:
282285
self._logger.log_layer_exposure(
283-
normal_user, layer, parameter_name, result
286+
normal_user, layer, parameter_name, result, sampling_rate=logged_sampling_rate,
287+
shadow_logged=shadow_logged,
288+
sampling_mode=_SDK_Configs.get_config_str_value("sampling_mode")
284289
)
285290

286291
layer = Layer._create(
@@ -490,7 +495,8 @@ def _verify_bg_threads_running(self):
490495
def __check_gate(self, user: StatsigUser, gate_name: str, log_exposure=True):
491496
user = self.__normalize_user(user)
492497
result = self._evaluator.check_gate(user, gate_name)
493-
should_log, logged_sampling_rate, shadow_logged = self.__determine_sampling("GATE", gate_name, result, user)
498+
should_log, logged_sampling_rate, shadow_logged = self.__determine_sampling(EntityType.GATE, gate_name, result,
499+
user)
494500

495501
if log_exposure and should_log:
496502
self._logger.log_gate_exposure(
@@ -511,7 +517,7 @@ def __get_config(self, user: StatsigUser, config_name: str, log_exposure=True):
511517

512518
result = self._evaluator.get_config(user, config_name)
513519
result.user = user
514-
should_log, logged_sampling_rate, shadow_logged = self.__determine_sampling("CONFIG", config_name,
520+
should_log, logged_sampling_rate, shadow_logged = self.__determine_sampling(EntityType.CONFIG, config_name,
515521
result, user)
516522

517523
if log_exposure and should_log:
@@ -527,47 +533,53 @@ def __get_config(self, user: StatsigUser, config_name: str, log_exposure=True):
527533
)
528534
return result
529535

530-
def __determine_sampling(self, type: str, name: str, result: _ConfigEvaluation,
531-
user: StatsigUser) -> Tuple[
536+
def __determine_sampling(self, type: EntityType, name: str, result: _ConfigEvaluation, user: StatsigUser,
537+
param_name="") -> Tuple[
532538
bool, Optional[int], Optional[str]]: # should_log, logged_sampling_rate, shadow_logged
533539
try:
534540
shadow_should_log, logged_sampling_rate = True, None
535541
env = self._options.get_sdk_environment_tier()
536542
sampling_mode = _SDK_Configs.get_config_str_value("sampling_mode")
537-
default_rule_id_sampling_rate = _SDK_Configs.get_config_int_value("default_rule_id_sampling_rate")
543+
special_case_sampling_rate = _SDK_Configs.get_config_int_value("special_case_sampling_rate")
538544

539545
if sampling_mode is None or sampling_mode == "none" or env != "production":
540-
return True, None, None
546+
return True, None, "logged"
541547

542-
if result.rule_id == "default" and result.forward_all_exposures:
543-
return True, None, None
548+
if result.forward_all_exposures:
549+
return True, None, "logged"
544550

545551
samplingSetKey = f"{name}_{result.rule_id}"
546552
if not self._sampling_key_set.contains(samplingSetKey):
547553
self._sampling_key_set.add(samplingSetKey)
548-
return True, None, None
554+
return True, None, "logged"
549555

550556
if result.sample_rate is not None:
551557
exposure_key = ""
552-
if type == "GATE":
558+
if type == EntityType.GATE:
553559
exposure_key = compute_dedupe_key_for_gate(name, result.rule_id, result.boolean_value,
554560
user.user_id, user.custom_ids)
555-
elif type == "CONFIG":
561+
elif type == EntityType.CONFIG:
556562
exposure_key = compute_dedupe_key_for_config(name, result.rule_id, user.user_id, user.custom_ids)
563+
elif type == EntityType.LAYER:
564+
exposure_key = compute_dedupe_key_for_layer(name, result.allocated_experiment, param_name,
565+
result.rule_id,
566+
user.user_id, user.custom_ids)
557567
shadow_should_log = is_hash_in_sampling_rate(exposure_key, result.sample_rate)
558568
logged_sampling_rate = result.sample_rate
559569

560-
if default_rule_id_sampling_rate is not None and result.rule_id == "default":
561-
shadow_should_log = is_hash_in_sampling_rate(name, default_rule_id_sampling_rate)
562-
logged_sampling_rate = default_rule_id_sampling_rate
570+
special_case_rules = ["disabled", "default", ""]
571+
572+
if result.rule_id in special_case_rules and special_case_sampling_rate is not None:
573+
shadow_should_log = is_hash_in_sampling_rate(name, special_case_sampling_rate)
574+
logged_sampling_rate = special_case_sampling_rate
563575

576+
shadow_logged = None if result.sample_rate is None else "logged" if shadow_should_log else "dropped"
564577
if sampling_mode == "on":
565-
return shadow_should_log, logged_sampling_rate, None
578+
return shadow_should_log, logged_sampling_rate, shadow_logged
566579
if sampling_mode == "shadow":
567-
shadow_logged = None if result.sample_rate is None else "logged" if shadow_should_log else "dropped"
568580
return True, logged_sampling_rate, shadow_logged
569581

570-
return True, None, None
582+
return True, None, "logged"
571583
except Exception as e:
572584
self._errorBoundary.log_exception("__determine_sampling", e, log_mode="debug")
573585
return True, None, None

testdata/download_config_specs_sampling.json

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,24 @@
4848
},
4949
"rules": [
5050
{
51-
"name": "1kNmlB23wylPFZi1M0Divl",
52-
"groupName": "statsig email",
51+
"name": "33qGYzVZr1MchRe4Ncj6MO",
5352
"passPercentage": 100,
5453
"conditions": [
5554
{
56-
"type": "user_field",
57-
"targetValue": [
58-
"@statsig.com"
59-
],
60-
"operator": "str_contains_any",
61-
"field": "email",
62-
"additionalValues": {}
55+
"type": "public",
56+
"targetValue": null,
57+
"operator": null,
58+
"field": null,
59+
"additionalValues": {},
60+
"isDeviceBased": false,
61+
"idType": "userID"
6362
}
6463
],
65-
"returnValue": {
66-
"number": 7,
67-
"string": "statsig",
68-
"boolean": false
69-
},
70-
"id": "1kNmlB23wylPFZi1M0Divl",
71-
"salt": "f2ac6975-174d-497e-be7f-599fea626132",
64+
"returnValue": {},
65+
"id": "33qGYzVZr1MchRe4Ncj6MO",
66+
"salt": "55a3430e-b239-4941-8208-951f5a9f8496",
67+
"isDeviceBased": false,
68+
"idType": "userID",
7269
"samplingRate": 101
7370
}
7471
]
@@ -1307,14 +1304,31 @@
13071304
]
13081305
}
13091306
],
1310-
"layer_configs": [],
1307+
"layers": {
1308+
"not_allocated_layer": []
1309+
},
1310+
"layer_configs": [
1311+
{
1312+
"name": "not_allocated_layer",
1313+
"type": "dynamic_config",
1314+
"salt": "b39af118-3f2c-4645-a4e4-7f7c96225ecc",
1315+
"enabled": true,
1316+
"defaultValue": {
1317+
"param": "ello"
1318+
},
1319+
"rules": [],
1320+
"isDeviceBased": false,
1321+
"idType": "userID",
1322+
"entity": "layer"
1323+
}
1324+
],
13111325
"has_updates": true,
13121326
"time": 1631638014811,
13131327
"id_lists": {
13141328
"list_1": true,
13151329
"list_2": true
13161330
},
13171331
"sdk_configs": {
1318-
"default_sampling_rate": 101
1332+
"special_case_sampling_rate": 101
13191333
}
13201334
}

0 commit comments

Comments
 (0)