|
20 | 20 | exception_is_already_captured, |
21 | 21 | mark_exception_as_captured, |
22 | 22 | ) |
23 | | -from posthog.feature_flags import InconclusiveMatchError, match_feature_flag_properties |
| 23 | +from posthog.feature_flags import ( |
| 24 | + InconclusiveMatchError, |
| 25 | + match_feature_flag_properties, |
| 26 | + build_dependency_graph, |
| 27 | + evaluate_flags_with_dependencies, |
| 28 | +) |
24 | 29 | from posthog.poller import Poller |
25 | 30 | from posthog.request import ( |
26 | 31 | DEFAULT_HOST, |
@@ -82,7 +87,10 @@ def get_identity_state(passed) -> tuple[str, bool]: |
82 | 87 | return (str(uuid4()), True) |
83 | 88 |
|
84 | 89 |
|
85 | | -def add_context_tags(properties): |
| 90 | +def add_context_tags(properties: Optional[Dict[str, Any]]) -> Dict[str, Any]: |
| 91 | + if properties is None: |
| 92 | + properties = {} |
| 93 | + |
86 | 94 | current_context = _get_current_context() |
87 | 95 | if current_context: |
88 | 96 | context_tags = current_context.collect_tags() |
@@ -179,6 +187,8 @@ def __init__( |
179 | 187 | self.timeout = timeout |
180 | 188 | self._feature_flags = None # private variable to store flags |
181 | 189 | self.feature_flags_by_key = None |
| 190 | + self.dependency_graph = None # for flag dependencies |
| 191 | + self.id_to_key_mapping = None # maps flag ID to flag key |
182 | 192 | self.group_type_mapping = None |
183 | 193 | self.cohorts = None |
184 | 194 | self.poll_interval = poll_interval |
@@ -304,6 +314,20 @@ def feature_flags(self, flags): |
304 | 314 | "feature_flags_by_key should be initialized when feature_flags is set" |
305 | 315 | ) |
306 | 316 |
|
| 317 | + # Build dependency graph for flag dependencies |
| 318 | + try: |
| 319 | + self.dependency_graph, self.id_to_key_mapping = build_dependency_graph( |
| 320 | + self._feature_flags |
| 321 | + ) |
| 322 | + self.log.debug( |
| 323 | + f"Built dependency graph with {len(self.dependency_graph.flags)} flags" |
| 324 | + ) |
| 325 | + self.log.debug(f"ID to key mapping: {self.id_to_key_mapping}") |
| 326 | + except Exception as e: |
| 327 | + self.log.warning(f"Failed to build dependency graph: {e}") |
| 328 | + self.dependency_graph = None |
| 329 | + self.id_to_key_mapping = None |
| 330 | + |
307 | 331 | def get_feature_variants( |
308 | 332 | self, |
309 | 333 | distinct_id, |
@@ -511,7 +535,7 @@ def capture( |
511 | 535 | if personless and "$process_person_profile" not in properties: |
512 | 536 | properties["$process_person_profile"] = False |
513 | 537 |
|
514 | | - msg = { |
| 538 | + msg: Dict[str, Any] = { |
515 | 539 | "properties": properties, |
516 | 540 | "timestamp": timestamp, |
517 | 541 | "distinct_id": distinct_id, |
@@ -1026,13 +1050,14 @@ def join(self): |
1026 | 1050 | posthog.join() |
1027 | 1051 | ``` |
1028 | 1052 | """ |
1029 | | - for consumer in self.consumers: |
1030 | | - consumer.pause() |
1031 | | - try: |
1032 | | - consumer.join() |
1033 | | - except RuntimeError: |
1034 | | - # consumer thread has not started |
1035 | | - pass |
| 1053 | + if self.consumers: |
| 1054 | + for consumer in self.consumers: |
| 1055 | + consumer.pause() |
| 1056 | + try: |
| 1057 | + consumer.join() |
| 1058 | + except RuntimeError: |
| 1059 | + # consumer thread has not started |
| 1060 | + pass |
1036 | 1061 |
|
1037 | 1062 | if self.poller: |
1038 | 1063 | self.poller.stop() |
@@ -1191,11 +1216,21 @@ def _compute_flag_locally( |
1191 | 1216 |
|
1192 | 1217 | focused_group_properties = group_properties[group_name] |
1193 | 1218 | return match_feature_flag_properties( |
1194 | | - feature_flag, groups[group_name], focused_group_properties |
| 1219 | + feature_flag, |
| 1220 | + groups[group_name], |
| 1221 | + focused_group_properties, |
| 1222 | + self.cohorts, |
| 1223 | + self.dependency_graph, |
| 1224 | + self.id_to_key_mapping, |
1195 | 1225 | ) |
1196 | 1226 | else: |
1197 | 1227 | return match_feature_flag_properties( |
1198 | | - feature_flag, distinct_id, person_properties, self.cohorts |
| 1228 | + feature_flag, |
| 1229 | + distinct_id, |
| 1230 | + person_properties, |
| 1231 | + self.cohorts, |
| 1232 | + self.dependency_graph, |
| 1233 | + self.id_to_key_mapping, |
1199 | 1234 | ) |
1200 | 1235 |
|
1201 | 1236 | def feature_enabled( |
@@ -1464,8 +1499,40 @@ def _locally_evaluate_flag( |
1464 | 1499 | assert self.feature_flags_by_key is not None, ( |
1465 | 1500 | "feature_flags_by_key should be initialized when feature_flags is set" |
1466 | 1501 | ) |
1467 | | - # Local evaluation |
| 1502 | + |
1468 | 1503 | flag = self.feature_flags_by_key.get(key) |
| 1504 | + if flag and flag.get("ensure_experience_continuity", False): |
| 1505 | + # Experience continuity flags cannot be evaluated locally |
| 1506 | + self.log.debug( |
| 1507 | + f"Flag {key} has experience continuity enabled, skipping local evaluation" |
| 1508 | + ) |
| 1509 | + return None |
| 1510 | + |
| 1511 | + # Check if any flags have dependencies |
| 1512 | + if self.dependency_graph and len(self.dependency_graph.flags) > 0: |
| 1513 | + # If we have dependencies, use the dependency-aware evaluation |
| 1514 | + try: |
| 1515 | + # Evaluate all flags with dependencies to ensure dependencies are available |
| 1516 | + all_results = evaluate_flags_with_dependencies( |
| 1517 | + self.feature_flags, |
| 1518 | + distinct_id, |
| 1519 | + person_properties, |
| 1520 | + self.cohorts, |
| 1521 | + requested_flag_keys={ |
| 1522 | + key |
| 1523 | + }, # Only evaluate the requested flag and its dependencies |
| 1524 | + ) |
| 1525 | + response = all_results.get(key) |
| 1526 | + if response is not None: |
| 1527 | + self.log.debug( |
| 1528 | + f"Successfully computed flag with dependencies: {key} -> {response}" |
| 1529 | + ) |
| 1530 | + return response |
| 1531 | + except Exception as e: |
| 1532 | + self.log.warning(f"Failed to evaluate flag with dependencies: {e}") |
| 1533 | + # Fall back to individual evaluation |
| 1534 | + |
| 1535 | + # Fall back to individual flag evaluation |
1469 | 1536 | if flag: |
1470 | 1537 | try: |
1471 | 1538 | response = self._compute_flag_locally( |
@@ -1644,7 +1711,7 @@ def _compute_payload_locally( |
1644 | 1711 | if self.feature_flags_by_key is None: |
1645 | 1712 | return payload |
1646 | 1713 |
|
1647 | | - flag_definition = self.feature_flags_by_key.get(key) |
| 1714 | + flag_definition = self.feature_flags_by_key.get(key) # type: ignore[unreachable] |
1648 | 1715 | if flag_definition: |
1649 | 1716 | flag_filters = flag_definition.get("filters") or {} |
1650 | 1717 | flag_payloads = flag_filters.get("payloads") or {} |
|
0 commit comments