|
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, |
@@ -179,6 +184,8 @@ def __init__( |
179 | 184 | self.timeout = timeout |
180 | 185 | self._feature_flags = None # private variable to store flags |
181 | 186 | self.feature_flags_by_key = None |
| 187 | + self.dependency_graph = None # for flag dependencies |
| 188 | + self.id_to_key_mapping = None # maps flag ID to flag key |
182 | 189 | self.group_type_mapping = None |
183 | 190 | self.cohorts = None |
184 | 191 | self.poll_interval = poll_interval |
@@ -304,6 +311,20 @@ def feature_flags(self, flags): |
304 | 311 | "feature_flags_by_key should be initialized when feature_flags is set" |
305 | 312 | ) |
306 | 313 |
|
| 314 | + # Build dependency graph for flag dependencies |
| 315 | + try: |
| 316 | + self.dependency_graph, self.id_to_key_mapping = build_dependency_graph( |
| 317 | + self._feature_flags |
| 318 | + ) |
| 319 | + self.log.debug( |
| 320 | + f"Built dependency graph with {len(self.dependency_graph.flags)} flags" |
| 321 | + ) |
| 322 | + self.log.debug(f"ID to key mapping: {self.id_to_key_mapping}") |
| 323 | + except Exception as e: |
| 324 | + self.log.warning(f"Failed to build dependency graph: {e}") |
| 325 | + self.dependency_graph = None |
| 326 | + self.id_to_key_mapping = None |
| 327 | + |
307 | 328 | def get_feature_variants( |
308 | 329 | self, |
309 | 330 | distinct_id, |
@@ -970,13 +991,14 @@ def join(self): |
970 | 991 | posthog.join() |
971 | 992 | ``` |
972 | 993 | """ |
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 |
| 994 | + if self.consumers: |
| 995 | + for consumer in self.consumers: |
| 996 | + consumer.pause() |
| 997 | + try: |
| 998 | + consumer.join() |
| 999 | + except RuntimeError: |
| 1000 | + # consumer thread has not started |
| 1001 | + pass |
980 | 1002 |
|
981 | 1003 | if self.poller: |
982 | 1004 | self.poller.stop() |
@@ -1135,11 +1157,21 @@ def _compute_flag_locally( |
1135 | 1157 |
|
1136 | 1158 | focused_group_properties = group_properties[group_name] |
1137 | 1159 | return match_feature_flag_properties( |
1138 | | - feature_flag, groups[group_name], focused_group_properties |
| 1160 | + feature_flag, |
| 1161 | + groups[group_name], |
| 1162 | + focused_group_properties, |
| 1163 | + self.cohorts, |
| 1164 | + self.dependency_graph, |
| 1165 | + self.id_to_key_mapping, |
1139 | 1166 | ) |
1140 | 1167 | else: |
1141 | 1168 | return match_feature_flag_properties( |
1142 | | - feature_flag, distinct_id, person_properties, self.cohorts |
| 1169 | + feature_flag, |
| 1170 | + distinct_id, |
| 1171 | + person_properties, |
| 1172 | + self.cohorts, |
| 1173 | + self.dependency_graph, |
| 1174 | + self.id_to_key_mapping, |
1143 | 1175 | ) |
1144 | 1176 |
|
1145 | 1177 | def feature_enabled( |
@@ -1408,8 +1440,58 @@ def _locally_evaluate_flag( |
1408 | 1440 | assert self.feature_flags_by_key is not None, ( |
1409 | 1441 | "feature_flags_by_key should be initialized when feature_flags is set" |
1410 | 1442 | ) |
1411 | | - # Local evaluation |
| 1443 | + |
| 1444 | + # Check if the requested flag has experience continuity enabled |
1412 | 1445 | flag = self.feature_flags_by_key.get(key) |
| 1446 | + if flag and flag.get("ensure_experience_continuity", False): |
| 1447 | + # For experience continuity flags, we must use individual evaluation |
| 1448 | + # which will properly raise InconclusiveMatchError and fall back to /decide |
| 1449 | + try: |
| 1450 | + response = self._compute_flag_locally( |
| 1451 | + flag, |
| 1452 | + distinct_id, |
| 1453 | + groups=groups, |
| 1454 | + person_properties=person_properties, |
| 1455 | + group_properties=group_properties, |
| 1456 | + ) |
| 1457 | + self.log.debug( |
| 1458 | + f"Successfully computed flag locally: {key} -> {response}" |
| 1459 | + ) |
| 1460 | + return response |
| 1461 | + except InconclusiveMatchError as e: |
| 1462 | + self.log.debug(f"Failed to compute flag {key} locally: {e}") |
| 1463 | + return None |
| 1464 | + except Exception as e: |
| 1465 | + self.log.exception( |
| 1466 | + f"[FEATURE FLAGS] Error while computing variant locally: {e}" |
| 1467 | + ) |
| 1468 | + return None |
| 1469 | + |
| 1470 | + # Check if any flags have dependencies |
| 1471 | + if self.dependency_graph and len(self.dependency_graph.flags) > 0: |
| 1472 | + # If we have dependencies, use the dependency-aware evaluation |
| 1473 | + try: |
| 1474 | + # Evaluate all flags with dependencies to ensure dependencies are available |
| 1475 | + all_results = evaluate_flags_with_dependencies( |
| 1476 | + self.feature_flags, |
| 1477 | + distinct_id, |
| 1478 | + person_properties, |
| 1479 | + self.cohorts, |
| 1480 | + requested_flag_keys={ |
| 1481 | + key |
| 1482 | + }, # Only evaluate the requested flag and its dependencies |
| 1483 | + ) |
| 1484 | + response = all_results.get(key) |
| 1485 | + if response is not None: |
| 1486 | + self.log.debug( |
| 1487 | + f"Successfully computed flag with dependencies: {key} -> {response}" |
| 1488 | + ) |
| 1489 | + return response |
| 1490 | + except Exception as e: |
| 1491 | + self.log.warning(f"Failed to evaluate flag with dependencies: {e}") |
| 1492 | + # Fall back to individual evaluation |
| 1493 | + |
| 1494 | + # Fall back to individual flag evaluation |
1413 | 1495 | if flag: |
1414 | 1496 | try: |
1415 | 1497 | response = self._compute_flag_locally( |
|
0 commit comments