Skip to content

Commit 5a0340e

Browse files
Add disable_ua_parser and disable_country_lookup options to reduce memory usage (#454)
using the kong eval project key - data for how much mem used for initialize UA + Country enabled: +24.2 MiB <img width="566" height="126" alt="Screenshot 2025-08-14 at 11 29 20 AM" src="https://github.com/user-attachments/assets/e042a5f8-c267-41e7-a25a-87eabc6c9b5e" /> Only UA disabled: +20.5 MiB <img width="631" height="219" alt="Screenshot 2025-08-14 at 11 57 10 AM" src="https://github.com/user-attachments/assets/1f2e19d0-05a8-4be4-9d91-8c5136df90cb" /> Only Country disabled: +8.6 MiB <img width="670" height="186" alt="Screenshot 2025-08-14 at 12 07 52 PM" src="https://github.com/user-attachments/assets/50b4300f-1b2c-42fb-8114-b4a4cca51abb" /> Disabling both: +6.7 MiB <img width="673" height="214" alt="Screenshot 2025-08-14 at 12 08 49 PM" src="https://github.com/user-attachments/assets/efda6376-258c-4259-bfb7-b5899675fa2a" /> Adds two new boolean options to `StatsigOptions` that allow users to disable UA parsing and country lookup functionality to reduce memory usage. When disabled, these features will print warning messages if accessed during evaluation but will not perform the actual parsing/lookup operations. **Link to Devin run**: https://app.devin.ai/sessions/3d3125c33eac43ecb454985bfd56ec59 **Requested by**: [email protected] --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]>
1 parent 0fe7713 commit 5a0340e

File tree

8 files changed

+2471
-2302
lines changed

8 files changed

+2471
-2302
lines changed

statsig/evaluation_details.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class DataSource(str, Enum):
1616
NETWORK = "Network"
1717
STATSIG_NETWORK = "StatsigNetwork"
1818
UNINITIALIZED = "Uninitialized"
19+
UA_NOT_LOADED = "UAParserNotLoaded"
20+
COUNTRY_NOT_LOADED = "CountryLookupNotLoaded"
1921

2022

2123
class EvaluationDetails:

statsig/evaluator.py

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from typing import Any, Dict, Optional, Union
77

88
from ip3country import CountryLookup
9-
from ua_parser import user_agent_parser
109

1110
from .client_initialize_formatter import ClientInitializeResponseFormatter
1211
from .config_evaluation import _ConfigEvaluation
@@ -17,18 +16,34 @@
1716
from .utils import HashingAlgorithm, JSONValue, sha256_hash
1817

1918

19+
def load_ua_parser():
20+
try:
21+
from ua_parser import user_agent_parser # pylint: disable=import-outside-toplevel
22+
return user_agent_parser
23+
except ImportError:
24+
logger.warning("ua_parser module not available")
25+
return None
26+
27+
2028
class _Evaluator:
21-
def __init__(self, spec_store: _SpecStore, global_custom_fields: Optional[Dict[str, JSONValue]]):
29+
def __init__(self, spec_store: _SpecStore, global_custom_fields: Optional[Dict[str, JSONValue]],
30+
disable_ua_parser: bool = False, disable_country_lookup: bool = False):
2231
self._spec_store = spec_store
2332
self._global_custom_fields = global_custom_fields
33+
self._disable_ua_parser = disable_ua_parser
34+
self._disable_country_lookup = disable_country_lookup
2435

2536
self._country_lookup: Optional[CountryLookup] = None
37+
self._ua_parser: Optional[Any] = None # Will be the ua_parser.user_agent_parser module
2638
self._gate_overrides: Dict[str, dict] = {}
2739
self._config_overrides: Dict[str, dict] = {}
2840
self._layer_overrides: Dict[str, dict] = {}
2941

3042
def initialize(self):
31-
self._country_lookup = CountryLookup()
43+
if not self._disable_country_lookup:
44+
self._country_lookup = CountryLookup()
45+
if not self._disable_ua_parser:
46+
self._ua_parser = load_ua_parser()
3247

3348
def override_gate(self, gate, value, user_id=None):
3449
gate_overrides = self._gate_overrides.get(gate)
@@ -120,6 +135,19 @@ def _create_evaluation_details(self,
120135
return EvaluationDetails(
121136
self._spec_store.last_update_time(), self._spec_store.initial_update_time, source, reason)
122137

138+
139+
def _update_evaluation_details_if_needed(self, end_result: _ConfigEvaluation) -> None:
140+
current_details = end_result.evaluation_details
141+
142+
if current_details is None:
143+
end_result.evaluation_details = self._create_evaluation_details()
144+
return
145+
146+
if current_details.source in [DataSource.UA_NOT_LOADED, DataSource.COUNTRY_NOT_LOADED]:
147+
return
148+
149+
end_result.evaluation_details = self._create_evaluation_details()
150+
123151
def __lookup_gate_override(self, user, gate):
124152
gate_overrides = self._gate_overrides.get(gate)
125153
if gate_overrides is None:
@@ -336,7 +364,11 @@ def __finalize_eval_result(self, config, end_result, did_pass, rule, is_nested=F
336364
if config.get("version", None) is not None:
337365
end_result.version = config.get("version")
338366

339-
end_result.evaluation_details = self._create_evaluation_details()
367+
if end_result.evaluation_details is not None and end_result.evaluation_details.source not in (
368+
DataSource.UA_NOT_LOADED, DataSource.COUNTRY_NOT_LOADED):
369+
end_result.evaluation_details = self._create_evaluation_details()
370+
371+
self._update_evaluation_details_if_needed(end_result)
340372

341373
if rule is None:
342374
end_result.json_value = config.get("defaultValue", {})
@@ -436,16 +468,22 @@ def __evaluate_condition(self, user, condition, end_result, sampling_rate=None):
436468
if value is None:
437469
ip = self.__get_from_user(user, "ip")
438470
if ip is not None and field == "country":
439-
if not self._country_lookup:
440-
self._country_lookup = CountryLookup()
441-
value = self._country_lookup.lookupStr(ip)
471+
if self._disable_country_lookup:
472+
logger.warning("Country lookup is disabled but was attempted during evaluation")
473+
end_result.evaluation_details = self._create_evaluation_details(
474+
EvaluationReason.none, DataSource.COUNTRY_NOT_LOADED)
475+
value = None
476+
else:
477+
if not self._country_lookup:
478+
self._country_lookup = CountryLookup()
479+
value = self._country_lookup.lookupStr(ip)
442480
if value is None:
443481
end_result.analytical_condition = sampling_rate is None
444482
return False
445483
elif type == "UA_BASED":
446484
value = self.__get_from_user(user, field)
447485
if value is None:
448-
value = self.__get_from_user_agent(user, field)
486+
value = self.__get_from_user_agent(user, field, end_result)
449487
elif type == "USER_FIELD":
450488
value = self.__get_from_user(user, field)
451489
elif type == "CURRENT_TIME":
@@ -754,11 +792,26 @@ def __get_value_as_float(self, input):
754792
return None
755793
return float(input)
756794

757-
def __get_from_user_agent(self, user, field):
795+
def __get_from_user_agent(self, user, field, end_result):
796+
if self._disable_ua_parser:
797+
logger.warning("UA parser is disabled but was attempted during evaluation")
798+
end_result.evaluation_details = self._create_evaluation_details(EvaluationReason.none,
799+
DataSource.UA_NOT_LOADED)
800+
return None
758801
ua = self.__get_from_user(user, "userAgent")
759802
if ua is None:
760803
return None
761-
parsed = user_agent_parser.Parse(ua)
804+
805+
try:
806+
if self._ua_parser is None:
807+
self._ua_parser = load_ua_parser()
808+
if self._ua_parser is None:
809+
return None
810+
parsed = self._ua_parser.Parse(ua)
811+
except Exception as e:
812+
logger.warning(f"Error parsing user agent: {e}")
813+
return None
814+
762815
field = field.lower()
763816
if field in ("osname", "os_name"):
764817
return parsed.get("os", {"family": None}).get("family")

statsig/statsig_options.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ def __init__(
116116
events_flushed_callback: Optional[
117117
Callable[[bool, List[Dict], Optional[int], Optional[Exception]], None]] = None,
118118
global_custom_fields: Optional[Dict[str, JSONValue]] = None,
119+
disable_ua_parser: bool = False,
120+
disable_country_lookup: bool = False,
119121
):
120122
self.data_store = data_store
121123
self._environment: Union[None, dict] = None
@@ -167,6 +169,8 @@ def __init__(
167169
self.events_flushed_callback = events_flushed_callback
168170
self._logging_copy: Dict[str, Any] = {}
169171
self.global_custom_fields = global_custom_fields
172+
self.disable_ua_parser = disable_ua_parser
173+
self.disable_country_lookup = disable_country_lookup
170174
self._set_logging_copy()
171175
self._attributes_changed = False
172176

@@ -250,5 +254,9 @@ def _set_logging_copy(self):
250254
logging_copy["sdk_error_callback"] = "SET"
251255
if self.global_custom_fields:
252256
logging_copy["global_custom_fields"] = to_raw_dict_or_none(self.global_custom_fields)
257+
if self.disable_ua_parser:
258+
logging_copy["disable_ua_parser"] = self.disable_ua_parser
259+
if self.disable_country_lookup:
260+
logging_copy["disable_country_lookup"] = self.disable_country_lookup
253261
self._logging_copy = logging_copy
254262
self._attributes_changed = False

statsig/statsig_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def _initialize_impl(self, sdk_key: str, options: Optional[StatsigOptions]):
124124
init_context
125125
)
126126

127-
self._evaluator = _Evaluator(self._spec_store, self._options.global_custom_fields)
127+
self._evaluator = _Evaluator(self._spec_store, self._options.global_custom_fields, self._options.disable_ua_parser, self._options.disable_country_lookup)
128128

129129
init_timeout = options.overall_init_timeout
130130
if init_timeout is not None:

test-requirements.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)