diff --git a/.sampo/changesets/doughty-baron-lemminkainen.md b/.sampo/changesets/doughty-baron-lemminkainen.md new file mode 100644 index 00000000..78e144a1 --- /dev/null +++ b/.sampo/changesets/doughty-baron-lemminkainen.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Use device IDs during local feature flag evaluation diff --git a/posthog/client.py b/posthog/client.py index 9469407b..438dc024 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -3373,6 +3373,7 @@ def evaluate_flags( person_properties=local_person_properties, group_properties=group_properties, flag_keys_to_evaluate=flag_keys, + device_id=device_id, ) feature_flags_by_key: Dict[str, Any] = self.feature_flags_by_key or {} diff --git a/posthog/test/test_evaluate_flags.py b/posthog/test/test_evaluate_flags.py index 9eb3a9fd..dec72b46 100644 --- a/posthog/test/test_evaluate_flags.py +++ b/posthog/test/test_evaluate_flags.py @@ -271,6 +271,55 @@ def test_empty_distinct_id_returns_empty_snapshot_without_events( self.assertEqual(len(feature_flag_called), 0) +class TestEvaluateFlagsLocalDeviceBucketing(unittest.TestCase): + def setUp(self): + self.client = Client(FAKE_TEST_API_KEY) + self.client.feature_flags = [ + { + "id": 1, + "key": "device-bucketed-flag", + "active": True, + "filters": { + "bucketing_identifier": "device_id", + # user-1 hashes above 50%, while both test device IDs hash below + # 50%, so these assertions prove device bucketing is used. + "groups": [{"properties": [], "rollout_percentage": 50}], + }, + } + ] + + def tearDown(self): + self.client.shutdown() + + @mock.patch("posthog.client.flags") + @mock.patch.object(Client, "capture") + def test_explicit_device_id_is_used_for_local_evaluation( + self, _patch_capture, patch_flags + ): + flags = self.client.evaluate_flags( + "user-1", + device_id="explicit-device", + only_evaluate_locally=True, + ) + + self.assertTrue(flags.get_flag("device-bucketed-flag")) + patch_flags.assert_not_called() + + @mock.patch("posthog.client.flags") + @mock.patch.object(Client, "capture") + def test_context_device_id_is_used_for_local_evaluation( + self, _patch_capture, patch_flags + ): + from posthog.contexts import new_context, set_context_device_id + + with new_context(): + set_context_device_id("context-device-0") + flags = self.client.evaluate_flags("user-1", only_evaluate_locally=True) + + self.assertTrue(flags.get_flag("device-bucketed-flag")) + patch_flags.assert_not_called() + + class TestEvaluateFlagsFiltering(unittest.TestCase): def setUp(self): self.client = Client(FAKE_TEST_API_KEY)