|
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 InconclusiveMatchError, match_feature_flag_properties, build_dependency_graph, evaluate_flags_with_dependencies |
24 | 24 | from posthog.poller import Poller |
25 | 25 | from posthog.request import ( |
26 | 26 | DEFAULT_HOST, |
@@ -179,6 +179,8 @@ def __init__( |
179 | 179 | self.timeout = timeout |
180 | 180 | self._feature_flags = None # private variable to store flags |
181 | 181 | self.feature_flags_by_key = None |
| 182 | + self.dependency_graph = None # for flag dependencies |
| 183 | + self.id_to_key_mapping = None # maps flag ID to flag key |
182 | 184 | self.group_type_mapping = None |
183 | 185 | self.cohorts = None |
184 | 186 | self.poll_interval = poll_interval |
@@ -303,6 +305,16 @@ def feature_flags(self, flags): |
303 | 305 | assert self.feature_flags_by_key is not None, ( |
304 | 306 | "feature_flags_by_key should be initialized when feature_flags is set" |
305 | 307 | ) |
| 308 | + |
| 309 | + # Build dependency graph for flag dependencies |
| 310 | + try: |
| 311 | + self.dependency_graph, self.id_to_key_mapping = build_dependency_graph(self._feature_flags) |
| 312 | + self.log.debug(f"Built dependency graph with {len(self.dependency_graph.flags)} flags") |
| 313 | + self.log.debug(f"ID to key mapping: {self.id_to_key_mapping}") |
| 314 | + except Exception as e: |
| 315 | + self.log.warning(f"Failed to build dependency graph: {e}") |
| 316 | + self.dependency_graph = None |
| 317 | + self.id_to_key_mapping = None |
306 | 318 |
|
307 | 319 | def get_feature_variants( |
308 | 320 | self, |
@@ -970,13 +982,14 @@ def join(self): |
970 | 982 | posthog.join() |
971 | 983 | ``` |
972 | 984 | """ |
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 |
| 985 | + if self.consumers: |
| 986 | + for consumer in self.consumers: |
| 987 | + consumer.pause() |
| 988 | + try: |
| 989 | + consumer.join() |
| 990 | + except RuntimeError: |
| 991 | + # consumer thread has not started |
| 992 | + pass |
980 | 993 |
|
981 | 994 | if self.poller: |
982 | 995 | self.poller.stop() |
@@ -1135,11 +1148,13 @@ def _compute_flag_locally( |
1135 | 1148 |
|
1136 | 1149 | focused_group_properties = group_properties[group_name] |
1137 | 1150 | return match_feature_flag_properties( |
1138 | | - feature_flag, groups[group_name], focused_group_properties |
| 1151 | + feature_flag, groups[group_name], focused_group_properties, self.cohorts, |
| 1152 | + self.dependency_graph, self.id_to_key_mapping |
1139 | 1153 | ) |
1140 | 1154 | else: |
1141 | 1155 | return match_feature_flag_properties( |
1142 | | - feature_flag, distinct_id, person_properties, self.cohorts |
| 1156 | + feature_flag, distinct_id, person_properties, self.cohorts, |
| 1157 | + self.dependency_graph, self.id_to_key_mapping |
1143 | 1158 | ) |
1144 | 1159 |
|
1145 | 1160 | def feature_enabled( |
@@ -1408,8 +1423,56 @@ def _locally_evaluate_flag( |
1408 | 1423 | assert self.feature_flags_by_key is not None, ( |
1409 | 1424 | "feature_flags_by_key should be initialized when feature_flags is set" |
1410 | 1425 | ) |
1411 | | - # Local evaluation |
| 1426 | + |
| 1427 | + # Check if the requested flag has experience continuity enabled |
1412 | 1428 | flag = self.feature_flags_by_key.get(key) |
| 1429 | + if flag and flag.get("ensure_experience_continuity", False): |
| 1430 | + # For experience continuity flags, we must use individual evaluation |
| 1431 | + # which will properly raise InconclusiveMatchError and fall back to /decide |
| 1432 | + try: |
| 1433 | + response = self._compute_flag_locally( |
| 1434 | + flag, |
| 1435 | + distinct_id, |
| 1436 | + groups=groups, |
| 1437 | + person_properties=person_properties, |
| 1438 | + group_properties=group_properties, |
| 1439 | + ) |
| 1440 | + self.log.debug( |
| 1441 | + f"Successfully computed flag locally: {key} -> {response}" |
| 1442 | + ) |
| 1443 | + return response |
| 1444 | + except InconclusiveMatchError as e: |
| 1445 | + self.log.debug(f"Failed to compute flag {key} locally: {e}") |
| 1446 | + return None |
| 1447 | + except Exception as e: |
| 1448 | + self.log.exception( |
| 1449 | + f"[FEATURE FLAGS] Error while computing variant locally: {e}" |
| 1450 | + ) |
| 1451 | + return None |
| 1452 | + |
| 1453 | + # Check if any flags have dependencies |
| 1454 | + if self.dependency_graph and len(self.dependency_graph.flags) > 0: |
| 1455 | + # If we have dependencies, use the dependency-aware evaluation |
| 1456 | + try: |
| 1457 | + # Evaluate all flags with dependencies to ensure dependencies are available |
| 1458 | + all_results = evaluate_flags_with_dependencies( |
| 1459 | + self.feature_flags, |
| 1460 | + distinct_id, |
| 1461 | + person_properties, |
| 1462 | + self.cohorts, |
| 1463 | + requested_flag_keys={key} # Only evaluate the requested flag and its dependencies |
| 1464 | + ) |
| 1465 | + response = all_results.get(key) |
| 1466 | + if response is not None: |
| 1467 | + self.log.debug( |
| 1468 | + f"Successfully computed flag with dependencies: {key} -> {response}" |
| 1469 | + ) |
| 1470 | + return response |
| 1471 | + except Exception as e: |
| 1472 | + self.log.warning(f"Failed to evaluate flag with dependencies: {e}") |
| 1473 | + # Fall back to individual evaluation |
| 1474 | + |
| 1475 | + # Fall back to individual flag evaluation |
1413 | 1476 | if flag: |
1414 | 1477 | try: |
1415 | 1478 | response = self._compute_flag_locally( |
|
0 commit comments