|
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, |
@@ -970,13 +994,14 @@ def join(self): |
970 | 994 | posthog.join() |
971 | 995 | ``` |
972 | 996 | """ |
973 | | - for consumer in self.consumers: |
974 | | - consumer.pause() |
975 | | - try: |
976 | | - consumer.join() |
977 | | - except RuntimeError: |
978 | | - # consumer thread has not started |
979 | | - pass |
| 997 | + if self.consumers: |
| 998 | + for consumer in self.consumers: |
| 999 | + consumer.pause() |
| 1000 | + try: |
| 1001 | + consumer.join() |
| 1002 | + except RuntimeError: |
| 1003 | + # consumer thread has not started |
| 1004 | + pass |
980 | 1005 |
|
981 | 1006 | if self.poller: |
982 | 1007 | self.poller.stop() |
@@ -1135,11 +1160,21 @@ def _compute_flag_locally( |
1135 | 1160 |
|
1136 | 1161 | focused_group_properties = group_properties[group_name] |
1137 | 1162 | return match_feature_flag_properties( |
1138 | | - feature_flag, groups[group_name], focused_group_properties |
| 1163 | + feature_flag, |
| 1164 | + groups[group_name], |
| 1165 | + focused_group_properties, |
| 1166 | + self.cohorts, |
| 1167 | + self.dependency_graph, |
| 1168 | + self.id_to_key_mapping, |
1139 | 1169 | ) |
1140 | 1170 | else: |
1141 | 1171 | return match_feature_flag_properties( |
1142 | | - feature_flag, distinct_id, person_properties, self.cohorts |
| 1172 | + feature_flag, |
| 1173 | + distinct_id, |
| 1174 | + person_properties, |
| 1175 | + self.cohorts, |
| 1176 | + self.dependency_graph, |
| 1177 | + self.id_to_key_mapping, |
1143 | 1178 | ) |
1144 | 1179 |
|
1145 | 1180 | def feature_enabled( |
@@ -1408,8 +1443,40 @@ def _locally_evaluate_flag( |
1408 | 1443 | assert self.feature_flags_by_key is not None, ( |
1409 | 1444 | "feature_flags_by_key should be initialized when feature_flags is set" |
1410 | 1445 | ) |
1411 | | - # Local evaluation |
| 1446 | + |
1412 | 1447 | flag = self.feature_flags_by_key.get(key) |
| 1448 | + if flag and flag.get("ensure_experience_continuity", False): |
| 1449 | + # Experience continuity flags cannot be evaluated locally |
| 1450 | + self.log.debug( |
| 1451 | + f"Flag {key} has experience continuity enabled, skipping local evaluation" |
| 1452 | + ) |
| 1453 | + return None |
| 1454 | + |
| 1455 | + # Check if any flags have dependencies |
| 1456 | + if self.dependency_graph and len(self.dependency_graph.flags) > 0: |
| 1457 | + # If we have dependencies, use the dependency-aware evaluation |
| 1458 | + try: |
| 1459 | + # Evaluate all flags with dependencies to ensure dependencies are available |
| 1460 | + all_results = evaluate_flags_with_dependencies( |
| 1461 | + self.feature_flags, |
| 1462 | + distinct_id, |
| 1463 | + person_properties, |
| 1464 | + self.cohorts, |
| 1465 | + requested_flag_keys={ |
| 1466 | + key |
| 1467 | + }, # Only evaluate the requested flag and its dependencies |
| 1468 | + ) |
| 1469 | + response = all_results.get(key) |
| 1470 | + if response is not None: |
| 1471 | + self.log.debug( |
| 1472 | + f"Successfully computed flag with dependencies: {key} -> {response}" |
| 1473 | + ) |
| 1474 | + return response |
| 1475 | + except Exception as e: |
| 1476 | + self.log.warning(f"Failed to evaluate flag with dependencies: {e}") |
| 1477 | + # Fall back to individual evaluation |
| 1478 | + |
| 1479 | + # Fall back to individual flag evaluation |
1413 | 1480 | if flag: |
1414 | 1481 | try: |
1415 | 1482 | response = self._compute_flag_locally( |
@@ -1588,7 +1655,7 @@ def _compute_payload_locally( |
1588 | 1655 | if self.feature_flags_by_key is None: |
1589 | 1656 | return payload |
1590 | 1657 |
|
1591 | | - flag_definition = self.feature_flags_by_key.get(key) |
| 1658 | + flag_definition = self.feature_flags_by_key.get(key) # type: ignore[unreachable] |
1592 | 1659 | if flag_definition: |
1593 | 1660 | flag_filters = flag_definition.get("filters") or {} |
1594 | 1661 | flag_payloads = flag_filters.get("payloads") or {} |
|
0 commit comments