Skip to content

Commit fcc255b

Browse files
Runtime rules engine (#148)
* bump to alpha version for python3 support This is the "official" repo, but has been virtually abandoned. the 2017 alpha release is what we need though * runtime rule NO MATCH ✅ * dry distinct id * DRY user context * helper to build context with runtime data * use helper everywhere for runtime data * ensure priority is given to new rule * test all use-cases * global import * Update mixpanel/flags/local_feature_flags.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update mixpanel/flags/test_local_feature_flags.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update mixpanel/flags/local_feature_flags.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update mixpanel/flags/local_feature_flags.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * unnest prod vs legacy comparisons * case-insensitivity ❌ * case-insensitivity ✅ * add tests for error cases * only lowercase leaf nodes of rule --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 71c22a2 commit fcc255b

File tree

4 files changed

+359
-60
lines changed

4 files changed

+359
-60
lines changed

‎mixpanel/flags/local_feature_flags.py‎

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import time
55
import threading
6+
import json_logic
67
from datetime import datetime, timedelta
78
from typing import Dict, Any, Callable, Optional
89
from .types import (
@@ -23,7 +24,6 @@
2324
logger = logging.getLogger(__name__)
2425
logging.getLogger("httpx").setLevel(logging.ERROR)
2526

26-
2727
class LocalFeatureFlagsProvider:
2828
FLAGS_DEFINITIONS_URL_PATH = "/flags/definitions"
2929

@@ -312,29 +312,82 @@ def _get_assigned_rollout(
312312
rollout_hash = normalized_hash(str(context_value), salt)
313313

314314
if (rollout_hash < rollout.rollout_percentage
315-
and self._is_runtime_evaluation_satisfied(rollout, context)
315+
and self._is_runtime_rules_engine_satisfied(rollout, context)
316316
):
317317
return rollout
318318

319319
return None
320+
321+
def lowercase_keys_and_values(self, val: Any) -> Any:
322+
if isinstance(val, str):
323+
return val.casefold()
324+
elif isinstance(val, list):
325+
return [self.lowercase_keys_and_values(item) for item in val]
326+
elif isinstance(val, dict):
327+
return {
328+
(key.casefold() if isinstance(key, str) else key):
329+
self.lowercase_keys_and_values(value)
330+
for key, value in val.items()
331+
}
332+
else:
333+
return val
334+
335+
def lowercase_only_leaf_nodes(self, val: Any) -> Dict[str, Any]:
336+
if isinstance(val, str):
337+
return val.casefold()
338+
elif isinstance(val, list):
339+
return [self.lowercase_only_leaf_nodes(item) for item in val]
340+
elif isinstance(val, dict):
341+
return {
342+
key:
343+
self.lowercase_only_leaf_nodes(value)
344+
for key, value in val.items()
345+
}
346+
else:
347+
return val
348+
349+
def _get_runtime_parameters(self, context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
350+
if not (custom_properties := context.get("custom_properties")):
351+
return None
352+
if not isinstance(custom_properties, dict):
353+
return None
354+
return self.lowercase_keys_and_values(custom_properties)
355+
356+
def _is_runtime_rules_engine_satisfied(self, rollout: Rollout, context: Dict[str, Any]) -> bool:
357+
if rollout.runtime_evaluation_rule:
358+
parameters_for_runtime_rule = self._get_runtime_parameters(context)
359+
if parameters_for_runtime_rule is None:
360+
return False
320361

321-
def _is_runtime_evaluation_satisfied(
362+
try:
363+
rule = self.lowercase_only_leaf_nodes(rollout.runtime_evaluation_rule)
364+
result = json_logic.jsonLogic(rule, parameters_for_runtime_rule)
365+
return bool(result)
366+
except Exception:
367+
logger.exception("Error evaluating runtime evaluation rule")
368+
return False
369+
370+
elif rollout.runtime_evaluation_definition: # legacy field supporting only exact match conditions
371+
return self._is_legacy_runtime_evaluation_rule_satisfied(rollout, context)
372+
373+
else:
374+
return True
375+
376+
def _is_legacy_runtime_evaluation_rule_satisfied(
322377
self, rollout: Rollout, context: Dict[str, Any]
323378
) -> bool:
324379
if not rollout.runtime_evaluation_definition:
325380
return True
326381

327-
if not (custom_properties := context.get("custom_properties")):
328-
return False
329-
330-
if not isinstance(custom_properties, dict):
382+
parameters_for_runtime_rule = self._get_runtime_parameters(context)
383+
if parameters_for_runtime_rule is None:
331384
return False
332385

333386
for key, expected_value in rollout.runtime_evaluation_definition.items():
334-
if key not in custom_properties:
387+
if key not in parameters_for_runtime_rule:
335388
return False
336389

337-
actual_value = custom_properties[key]
390+
actual_value = parameters_for_runtime_rule[key]
338391
if actual_value.casefold() != expected_value.casefold():
339392
return False
340393

0 commit comments

Comments
 (0)