|
2 | 2 | from unittest import mock |
3 | 3 |
|
4 | 4 | from posthog.client import Client |
5 | | -from posthog.test.test_utils import FAKE_TEST_API_KEY |
| 5 | +from posthog.test.test_utils import FAKE_TEST_API_KEY, FakeRedis |
6 | 6 | from posthog.types import ( |
7 | 7 | FeatureFlag, |
8 | 8 | FeatureFlagError, |
9 | 9 | FeatureFlagResult, |
10 | 10 | FlagMetadata, |
11 | 11 | FlagReason, |
12 | 12 | ) |
| 13 | +from posthog.utils import RedisFlagCache |
13 | 14 |
|
14 | 15 |
|
15 | 16 | class TestFeatureFlagResult(unittest.TestCase): |
@@ -740,6 +741,103 @@ def _populate_stale_cache(self, distinct_id, flag_key, flag_result): |
740 | 741 | flag_definition_version=self.client.flag_definition_version, |
741 | 742 | ) |
742 | 743 |
|
| 744 | + @mock.patch("posthog.client.flags") |
| 745 | + @mock.patch.object(Client, "capture") |
| 746 | + def test_redis_timeout_returns_stale_cached_value_and_event( |
| 747 | + self, patch_capture, patch_flags |
| 748 | + ): |
| 749 | + """Redis stale fallback reconstructs the result used by flag-called events.""" |
| 750 | + from posthog.request import RequestsTimeout |
| 751 | + |
| 752 | + self.client.flag_cache = RedisFlagCache(FakeRedis()) |
| 753 | + cached_result = FeatureFlagResult( |
| 754 | + key="my-flag", |
| 755 | + enabled=True, |
| 756 | + variant="cached-variant", |
| 757 | + payload={"from": "redis"}, |
| 758 | + reason="cached reason", |
| 759 | + ) |
| 760 | + self._populate_stale_cache("some-distinct-id", "my-flag", cached_result) |
| 761 | + patch_flags.side_effect = RequestsTimeout("Request timed out") |
| 762 | + |
| 763 | + flag_result = self.client.get_feature_flag_result("my-flag", "some-distinct-id") |
| 764 | + |
| 765 | + self.assertEqual(flag_result, cached_result) |
| 766 | + patch_capture.assert_called_once_with( |
| 767 | + "$feature_flag_called", |
| 768 | + distinct_id="some-distinct-id", |
| 769 | + properties={ |
| 770 | + "$feature_flag": "my-flag", |
| 771 | + "$feature_flag_response": "cached-variant", |
| 772 | + "locally_evaluated": False, |
| 773 | + "$feature/my-flag": "cached-variant", |
| 774 | + "$feature_flag_payload": {"from": "redis"}, |
| 775 | + "$feature_flag_error": FeatureFlagError.TIMEOUT, |
| 776 | + }, |
| 777 | + groups={}, |
| 778 | + disable_geoip=None, |
| 779 | + ) |
| 780 | + |
| 781 | + @mock.patch("posthog.client.flags") |
| 782 | + @mock.patch.object(Client, "capture") |
| 783 | + def test_redis_timeout_returns_stale_cached_value_without_event( |
| 784 | + self, patch_capture, patch_flags |
| 785 | + ): |
| 786 | + """Redis stale fallback is reconstructed when event capture is disabled.""" |
| 787 | + from posthog.request import RequestsTimeout |
| 788 | + |
| 789 | + self.client.flag_cache = RedisFlagCache(FakeRedis()) |
| 790 | + cached_result = FeatureFlagResult( |
| 791 | + key="my-flag", |
| 792 | + enabled=False, |
| 793 | + variant=None, |
| 794 | + payload={"from": "redis"}, |
| 795 | + reason="cached reason", |
| 796 | + ) |
| 797 | + self._populate_stale_cache("some-distinct-id", "my-flag", cached_result) |
| 798 | + patch_flags.side_effect = RequestsTimeout("Request timed out") |
| 799 | + |
| 800 | + flag_result = self.client.get_feature_flag_result( |
| 801 | + "my-flag", "some-distinct-id", send_feature_flag_events=False |
| 802 | + ) |
| 803 | + |
| 804 | + self.assertEqual(flag_result, cached_result) |
| 805 | + patch_capture.assert_not_called() |
| 806 | + |
| 807 | + @mock.patch("posthog.client.flags") |
| 808 | + @mock.patch.object(Client, "capture") |
| 809 | + def test_legacy_redis_entry_is_safely_ignored(self, patch_capture, patch_flags): |
| 810 | + """Unmarked entries from older SDKs are not returned as result dictionaries.""" |
| 811 | + import json |
| 812 | + import time |
| 813 | + |
| 814 | + from posthog.request import RequestsTimeout |
| 815 | + |
| 816 | + redis = FakeRedis() |
| 817 | + self.client.flag_cache = RedisFlagCache(redis) |
| 818 | + cache_key = self.client.flag_cache._get_cache_key("some-distinct-id", "my-flag") |
| 819 | + redis.store[cache_key] = json.dumps( |
| 820 | + { |
| 821 | + "flag_result": { |
| 822 | + "key": "my-flag", |
| 823 | + "enabled": True, |
| 824 | + "variant": None, |
| 825 | + "payload": {"from": "legacy"}, |
| 826 | + "reason": None, |
| 827 | + }, |
| 828 | + "flag_version": self.client.flag_definition_version, |
| 829 | + "timestamp": time.time(), |
| 830 | + } |
| 831 | + ) |
| 832 | + patch_flags.side_effect = RequestsTimeout("Request timed out") |
| 833 | + |
| 834 | + flag_result = self.client.get_feature_flag_result( |
| 835 | + "my-flag", "some-distinct-id", send_feature_flag_events=False |
| 836 | + ) |
| 837 | + |
| 838 | + self.assertIsNone(flag_result) |
| 839 | + patch_capture.assert_not_called() |
| 840 | + |
743 | 841 | @mock.patch("posthog.client.flags") |
744 | 842 | @mock.patch.object(Client, "capture") |
745 | 843 | def test_timeout_error_returns_stale_cached_value(self, patch_capture, patch_flags): |
|
0 commit comments