import time import unittest from datetime import datetime from uuid import uuid4 import mock import six from parameterized import parameterized from posthog.client import Client from posthog.contexts import get_context_session_id, new_context, set_context_session from posthog.request import APIError from posthog.test.test_utils import FAKE_TEST_API_KEY from posthog.types import FeatureFlag, LegacyFlagMetadata from posthog.version import VERSION class TestClient(unittest.TestCase): @classmethod def setUpClass(cls): # This ensures no real HTTP POST requests are made cls.client_post_patcher = mock.patch("posthog.client.batch_post") cls.consumer_post_patcher = mock.patch("posthog.consumer.batch_post") cls.client_post_patcher.start() cls.consumer_post_patcher.start() @classmethod def tearDownClass(cls): cls.client_post_patcher.stop() cls.consumer_post_patcher.stop() def set_fail(self, e, batch): """Mark the failure handler""" print("FAIL", e, batch) # noqa: T201 self.failed = True def setUp(self): self.failed = False self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail) def test_requires_api_key(self): self.assertRaises(TypeError, Client) def test_empty_flush(self): self.client.flush() def test_basic_capture(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.capture("python test event", distinct_id="distinct_id") self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) # these will change between platforms so just asssert on presence here assert msg["properties"]["$python_runtime"] == mock.ANY assert msg["properties"]["$python_version"] == mock.ANY assert msg["properties"]["$os"] == mock.ANY assert msg["properties"]["$os_version"] == mock.ANY def test_basic_capture_with_uuid(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) uuid = str(uuid4()) msg_uuid = client.capture( "python test event", distinct_id="distinct_id", uuid=uuid ) self.assertEqual(msg_uuid, uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertEqual(msg["uuid"], uuid) self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) def test_basic_capture_with_project_api_key(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client( project_api_key=FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True, ) msg_uuid = client.capture("python test event", distinct_id="distinct_id") self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) def test_basic_super_properties(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, super_properties={"source": "repo-name"}, sync_mode=True, ) msg_uuid = client.capture("python test event", distinct_id="distinct_id") self.assertIsNotNone(msg_uuid) # Check the enqueued message mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertEqual(msg["properties"]["source"], "repo-name") def test_basic_capture_exception(self): with mock.patch.object(Client, "capture", return_value=None) as patch_capture: client = self.client exception = Exception("test exception") client.capture_exception(exception, distinct_id="distinct_id") self.assertTrue(patch_capture.called) capture_call = patch_capture.call_args self.assertEqual(capture_call[0][0], "$exception") self.assertEqual(capture_call[1]["distinct_id"], "distinct_id") def test_basic_capture_exception_with_distinct_id(self): with mock.patch.object(Client, "capture", return_value=None) as patch_capture: client = self.client exception = Exception("test exception") client.capture_exception(exception, distinct_id="distinct_id") self.assertTrue(patch_capture.called) capture_call = patch_capture.call_args self.assertEqual(capture_call[0][0], "$exception") self.assertEqual(capture_call[1]["distinct_id"], "distinct_id") def test_basic_capture_exception_with_correct_host_generation(self): with mock.patch.object(Client, "capture", return_value=None) as patch_capture: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://aloha.com" ) exception = Exception("test exception") client.capture_exception(exception, distinct_id="distinct_id") self.assertTrue(patch_capture.called) call = patch_capture.call_args self.assertEqual(call[0][0], "$exception") self.assertEqual(call[1]["distinct_id"], "distinct_id") def test_basic_capture_exception_with_correct_host_generation_for_server_hosts( self, ): with mock.patch.object(Client, "capture", return_value=None) as patch_capture: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://app.posthog.com", ) exception = Exception("test exception") client.capture_exception(exception, distinct_id="distinct_id") self.assertTrue(patch_capture.called) capture_call = patch_capture.call_args self.assertEqual(capture_call[0][0], "$exception") self.assertEqual(capture_call[1]["distinct_id"], "distinct_id") def test_basic_capture_exception_with_no_exception_given(self): with mock.patch.object(Client, "capture", return_value=None) as patch_capture: client = self.client try: raise Exception("test exception") except Exception: client.capture_exception(None, distinct_id="distinct_id") self.assertTrue(patch_capture.called) capture_call = patch_capture.call_args print(capture_call) self.assertEqual(capture_call[1]["distinct_id"], "distinct_id") self.assertEqual(capture_call[0][0], "$exception") self.assertEqual( capture_call[1]["properties"]["$exception_type"], "Exception" ) self.assertEqual( capture_call[1]["properties"]["$exception_message"], "test exception" ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["mechanism"][ "type" ], "generic", ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["mechanism"][ "handled" ], True, ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["module"], None ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["type"], "Exception" ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["value"], "test exception", ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][ "type" ], "raw", ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][ "frames" ][0]["filename"], "posthog/test/test_client.py", ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][ "frames" ][0]["function"], "test_basic_capture_exception_with_no_exception_given", ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][ "frames" ][0]["module"], "posthog.test.test_client", ) self.assertEqual( capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][ "frames" ][0]["in_app"], True, ) def test_basic_capture_exception_with_no_exception_happening(self): with mock.patch.object(Client, "capture", return_value=None) as patch_capture: with self.assertLogs("posthog", level="WARNING") as logs: client = self.client client.capture_exception(None) self.assertFalse(patch_capture.called) self.assertEqual( logs.output[0], "WARNING:posthog:No exception information available", ) def test_capture_exception_logs_when_enabled(self): client = Client(FAKE_TEST_API_KEY, log_captured_exceptions=True) with self.assertLogs("posthog", level="ERROR") as logs: client.capture_exception( Exception("test exception"), distinct_id="distinct_id" ) self.assertEqual( logs.output[0], "ERROR:posthog:test exception\nNoneType: None" ) @mock.patch("posthog.client.flags") def test_basic_capture_with_feature_flags(self, patch_flags): patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}} with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) msg_uuid = client.capture( "python test event", distinct_id="distinct_id", send_feature_flags=True ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) self.assertEqual( msg["properties"]["$feature/beta-feature"], "random-variant" ) self.assertEqual( msg["properties"]["$active_feature_flags"], ["beta-feature"] ) self.assertEqual(patch_flags.call_count, 1) @mock.patch("posthog.client.flags") def test_basic_capture_with_locally_evaluated_feature_flags(self, patch_flags): patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}} multivariate_flag = { "id": 1, "name": "Beta Feature", "key": "beta-feature-local", "active": True, "rollout_percentage": 100, "filters": { "groups": [ { "properties": [ { "key": "email", "type": "person", "value": "test@posthog.com", "operator": "exact", } ], "rollout_percentage": 100, }, { "rollout_percentage": 50, }, ], "multivariate": { "variants": [ { "key": "first-variant", "name": "First Variant", "rollout_percentage": 50, }, { "key": "second-variant", "name": "Second Variant", "rollout_percentage": 25, }, { "key": "third-variant", "name": "Third Variant", "rollout_percentage": 25, }, ] }, "payloads": { "first-variant": "some-payload", "third-variant": {"a": "json"}, }, }, } basic_flag = { "id": 1, "name": "Beta Feature", "key": "person-flag", "active": True, "filters": { "groups": [ { "properties": [ { "key": "region", "operator": "exact", "value": ["USA"], "type": "person", } ], "rollout_percentage": 100, } ], "payloads": {"true": 300}, }, } false_flag = { "id": 1, "name": "Beta Feature", "key": "false-flag", "active": True, "filters": { "groups": [ { "properties": [], "rollout_percentage": 0, } ], "payloads": {"true": 300}, }, } with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) client.feature_flags = [multivariate_flag, basic_flag, false_flag] msg_uuid = client.capture("python test event", distinct_id="distinct_id") self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) self.assertEqual( msg["properties"]["$feature/beta-feature-local"], "third-variant" ) self.assertEqual(msg["properties"]["$feature/false-flag"], False) self.assertEqual( msg["properties"]["$active_feature_flags"], ["beta-feature-local"] ) assert "$feature/beta-feature" not in msg["properties"] self.assertEqual(patch_flags.call_count, 0) # test that flags are not evaluated without local evaluation with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) client.feature_flags = [] msg_uuid = client.capture("python test event", distinct_id="distinct_id") self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] assert "$feature/beta-feature" not in msg["properties"] assert "$feature/beta-feature-local" not in msg["properties"] assert "$feature/false-flag" not in msg["properties"] assert "$active_feature_flags" not in msg["properties"] @mock.patch("posthog.client.get") def test_load_feature_flags_quota_limited(self, patch_get): mock_response = { "type": "quota_limited", "detail": "You have exceeded your feature flag request quota", "code": "payment_required", } patch_get.side_effect = APIError(402, mock_response["detail"]) client = Client(FAKE_TEST_API_KEY, personal_api_key="test") with self.assertLogs("posthog", level="WARNING") as logs: client._load_feature_flags() self.assertEqual(client.feature_flags, []) self.assertEqual(client.feature_flags_by_key, {}) self.assertEqual(client.group_type_mapping, {}) self.assertEqual(client.cohorts, {}) self.assertIn("PostHog feature flags quota limited", logs.output[0]) @mock.patch("posthog.client.flags") def test_dont_override_capture_with_local_flags(self, patch_flags): patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}} multivariate_flag = { "id": 1, "name": "Beta Feature", "key": "beta-feature-local", "active": True, "rollout_percentage": 100, "filters": { "groups": [ { "properties": [ { "key": "email", "type": "person", "value": "test@posthog.com", "operator": "exact", } ], "rollout_percentage": 100, }, { "rollout_percentage": 50, }, ], "multivariate": { "variants": [ { "key": "first-variant", "name": "First Variant", "rollout_percentage": 50, }, { "key": "second-variant", "name": "Second Variant", "rollout_percentage": 25, }, { "key": "third-variant", "name": "Third Variant", "rollout_percentage": 25, }, ] }, "payloads": { "first-variant": "some-payload", "third-variant": {"a": "json"}, }, }, } basic_flag = { "id": 1, "name": "Beta Feature", "key": "person-flag", "active": True, "filters": { "groups": [ { "properties": [ { "key": "region", "operator": "exact", "value": ["USA"], "type": "person", } ], "rollout_percentage": 100, } ], "payloads": {"true": 300}, }, } with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) client.feature_flags = [multivariate_flag, basic_flag] msg_uuid = client.capture( "python test event", distinct_id="distinct_id", properties={"$feature/beta-feature-local": "my-custom-variant"}, ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) self.assertEqual( msg["properties"]["$feature/beta-feature-local"], "my-custom-variant" ) self.assertEqual( msg["properties"]["$active_feature_flags"], ["beta-feature-local"] ) assert "$feature/beta-feature" not in msg["properties"] assert "$feature/person-flag" not in msg["properties"] self.assertEqual(patch_flags.call_count, 0) @mock.patch("posthog.client.flags") def test_basic_capture_with_feature_flags_returns_active_only(self, patch_flags): patch_flags.return_value = { "featureFlags": { "beta-feature": "random-variant", "alpha-feature": True, "off-feature": False, } } with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) msg_uuid = client.capture( "python test event", distinct_id="distinct_id", send_feature_flags=True ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) self.assertEqual(msg["distinct_id"], "distinct_id") self.assertTrue(msg["properties"]["$geoip_disable"]) self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) self.assertEqual( msg["properties"]["$feature/beta-feature"], "random-variant" ) self.assertEqual(msg["properties"]["$feature/alpha-feature"], True) self.assertEqual( msg["properties"]["$active_feature_flags"], ["beta-feature", "alpha-feature"], ) self.assertEqual(patch_flags.call_count, 1) patch_flags.assert_called_with( "random_key", "https://us.i.posthog.com", timeout=3, distinct_id="distinct_id", groups={}, person_properties={}, group_properties={}, geoip_disable=True, ) @mock.patch("posthog.client.flags") def test_basic_capture_with_feature_flags_and_disable_geoip_returns_correctly( self, patch_flags ): patch_flags.return_value = { "featureFlags": { "beta-feature": "random-variant", "alpha-feature": True, "off-feature": False, } } with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, host="https://app.posthog.com", on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, disable_geoip=True, feature_flags_request_timeout_seconds=12, sync_mode=True, ) msg_uuid = client.capture( "python test event", distinct_id="distinct_id", send_feature_flags=True, disable_geoip=False, ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) self.assertTrue("$geoip_disable" not in msg["properties"]) self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) self.assertEqual( msg["properties"]["$feature/beta-feature"], "random-variant" ) self.assertEqual(msg["properties"]["$feature/alpha-feature"], True) self.assertEqual( msg["properties"]["$active_feature_flags"], ["beta-feature", "alpha-feature"], ) self.assertEqual(patch_flags.call_count, 1) patch_flags.assert_called_with( "random_key", "https://us.i.posthog.com", timeout=12, distinct_id="distinct_id", groups={}, person_properties={}, group_properties={}, geoip_disable=False, ) @mock.patch("posthog.client.flags") def test_basic_capture_with_feature_flags_switched_off_doesnt_send_them( self, patch_flags ): patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}} with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) msg_uuid = client.capture( "python test event", distinct_id="distinct_id", send_feature_flags=False ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) self.assertTrue("$feature/beta-feature" not in msg["properties"]) self.assertTrue("$active_feature_flags" not in msg["properties"]) self.assertEqual(patch_flags.call_count, 0) @mock.patch("posthog.client.flags") def test_capture_with_send_feature_flags_options_only_evaluate_locally_true( self, patch_flags ): """Test that SendFeatureFlagsOptions with only_evaluate_locally=True uses local evaluation""" with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) # Set up local flags client.feature_flags = [ { "id": 1, "key": "local-flag", "active": True, "filters": { "groups": [ { "properties": [{"key": "region", "value": "US"}], "rollout_percentage": 100, } ], }, } ] send_options = { "only_evaluate_locally": True, "person_properties": {"region": "US"}, } msg_uuid = client.capture( "test event", distinct_id="distinct_id", send_feature_flags=send_options ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Verify flags() was not called (no remote evaluation) patch_flags.assert_not_called() # Check the message includes the local flag mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["properties"]["$feature/local-flag"], True) self.assertEqual(msg["properties"]["$active_feature_flags"], ["local-flag"]) @mock.patch("posthog.client.flags") def test_capture_with_send_feature_flags_options_only_evaluate_locally_false( self, patch_flags ): """Test that SendFeatureFlagsOptions with only_evaluate_locally=False forces remote evaluation""" patch_flags.return_value = {"featureFlags": {"remote-flag": "remote-value"}} with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) send_options = { "only_evaluate_locally": False, "person_properties": {"plan": "premium"}, "group_properties": {"company": {"type": "enterprise"}}, } msg_uuid = client.capture( "test event", distinct_id="distinct_id", groups={"company": "acme"}, send_feature_flags=send_options, ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Verify flags() was called with the correct properties patch_flags.assert_called_once() call_args = patch_flags.call_args[1] self.assertEqual(call_args["person_properties"], {"plan": "premium"}) self.assertEqual( call_args["group_properties"], {"company": {"type": "enterprise"}} ) # Check the message includes the remote flag mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["properties"]["$feature/remote-flag"], "remote-value") @mock.patch("posthog.client.flags") def test_capture_with_send_feature_flags_options_default_behavior( self, patch_flags ): """Test that SendFeatureFlagsOptions without only_evaluate_locally defaults to remote evaluation""" patch_flags.return_value = {"featureFlags": {"default-flag": "default-value"}} with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) send_options = { "person_properties": {"subscription": "pro"}, } msg_uuid = client.capture( "test event", distinct_id="distinct_id", send_feature_flags=send_options ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Verify flags() was called (default to remote evaluation) patch_flags.assert_called_once() call_args = patch_flags.call_args[1] self.assertEqual(call_args["person_properties"], {"subscription": "pro"}) # Check the message includes the flag mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual( msg["properties"]["$feature/default-flag"], "default-value" ) @mock.patch("posthog.client.flags") def test_capture_exception_with_send_feature_flags_options(self, patch_flags): """Test that capture_exception also supports SendFeatureFlagsOptions""" patch_flags.return_value = {"featureFlags": {"exception-flag": True}} with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, sync_mode=True, ) send_options = { "only_evaluate_locally": False, "person_properties": {"user_type": "admin"}, } try: raise ValueError("Test exception") except ValueError as e: msg_uuid = client.capture_exception( e, distinct_id="distinct_id", send_feature_flags=send_options ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Verify flags() was called with the correct properties patch_flags.assert_called_once() call_args = patch_flags.call_args[1] self.assertEqual(call_args["person_properties"], {"user_type": "admin"}) # Check the message includes the flag mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "$exception") self.assertEqual(msg["properties"]["$feature/exception-flag"], True) def test_stringifies_distinct_id(self): # A large number that loses precision in node: # node -e "console.log(157963456373623802 + 1)" > 157963456373623800 with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.capture( "python test event", distinct_id=157963456373623802 ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["distinct_id"], "157963456373623802") def test_advanced_capture(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.capture( "python test event", distinct_id="distinct_id", properties={"property": "value"}, timestamp=datetime(2014, 9, 3), uuid="new-uuid", ) self.assertEqual(msg_uuid, "new-uuid") # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00") self.assertEqual(msg["properties"]["property"], "value") self.assertEqual(msg["event"], "python test event") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) self.assertEqual(msg["uuid"], "new-uuid") self.assertEqual(msg["distinct_id"], "distinct_id") self.assertTrue("$groups" not in msg["properties"]) def test_groups_capture(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.capture( "test_event", distinct_id="distinct_id", groups={"company": "id:5", "instance": "app.posthog.com"}, ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual( msg["properties"]["$groups"], {"company": "id:5", "instance": "app.posthog.com"}, ) def test_basic_set(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.set( distinct_id="distinct_id", properties={"trait": "value"} ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["$set"]["trait"], "value") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) self.assertEqual(msg["distinct_id"], "distinct_id") def test_advanced_set(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.set( distinct_id="distinct_id", properties={"trait": "value"}, timestamp=datetime(2014, 9, 3), uuid="new-uuid", ) self.assertEqual(msg_uuid, "new-uuid") # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00") self.assertEqual(msg["$set"]["trait"], "value") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) self.assertTrue(isinstance(msg["timestamp"], str)) self.assertEqual(msg["uuid"], "new-uuid") self.assertEqual(msg["distinct_id"], "distinct_id") def test_basic_set_once(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.set_once( distinct_id="distinct_id", properties={"trait": "value"} ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["$set_once"]["trait"], "value") self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) self.assertEqual(msg["distinct_id"], "distinct_id") def test_advanced_set_once(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.set_once( distinct_id="distinct_id", properties={"trait": "value"}, timestamp=datetime(2014, 9, 3), uuid="new-uuid", ) self.assertEqual(msg_uuid, "new-uuid") # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00") self.assertEqual(msg["$set_once"]["trait"], "value") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) self.assertTrue(isinstance(msg["timestamp"], str)) self.assertEqual(msg["uuid"], "new-uuid") self.assertEqual(msg["distinct_id"], "distinct_id") def test_basic_group_identify(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.group_identify("organization", "id:5") self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "$groupidentify") self.assertEqual( msg["properties"], { "$group_type": "organization", "$group_key": "id:5", "$group_set": {}, "$lib": "posthog-python", "$lib_version": VERSION, "$geoip_disable": True, }, ) self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) def test_basic_group_identify_with_distinct_id(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.group_identify( "organization", "id:5", distinct_id="distinct_id" ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "$groupidentify") self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual( msg["properties"], { "$group_type": "organization", "$group_key": "id:5", "$group_set": {}, "$lib": "posthog-python", "$lib_version": VERSION, "$geoip_disable": True, }, ) self.assertTrue(isinstance(msg["timestamp"], str)) self.assertIsNotNone(msg.get("uuid")) def test_advanced_group_identify(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.group_identify( "organization", "id:5", {"trait": "value"}, timestamp=datetime(2014, 9, 3), uuid="new-uuid", ) self.assertEqual(msg_uuid, "new-uuid") # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "$groupidentify") self.assertEqual( msg["properties"], { "$group_type": "organization", "$group_key": "id:5", "$group_set": {"trait": "value"}, "$lib": "posthog-python", "$lib_version": VERSION, "$geoip_disable": True, }, ) self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00") def test_advanced_group_identify_with_distinct_id(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.group_identify( "organization", "id:5", {"trait": "value"}, timestamp=datetime(2014, 9, 3), uuid="new-uuid", distinct_id="distinct_id", ) self.assertEqual(msg_uuid, "new-uuid") # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "$groupidentify") self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual( msg["properties"], { "$group_type": "organization", "$group_key": "id:5", "$group_set": {"trait": "value"}, "$lib": "posthog-python", "$lib_version": VERSION, "$geoip_disable": True, }, ) self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00") def test_basic_alias(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) msg_uuid = client.alias("previousId", "distinct_id") self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["properties"]["distinct_id"], "previousId") self.assertEqual(msg["properties"]["alias"], "distinct_id") @parameterized.expand( [ # test_name, session_id, additional_properties, expected_properties ("basic_session_id", "test-session-123", {}, {}), ( "session_id_with_other_properties", "test-session-456", { "custom_prop": "custom_value", "$process_person_profile": False, "$current_url": "https://example.com", }, { "custom_prop": "custom_value", "$process_person_profile": False, "$current_url": "https://example.com", }, ), ("session_id_uuid_format", str(uuid4()), {}, {}), ("session_id_numeric_string", "1234567890", {}, {}), ("session_id_empty_string", "", {}, {}), ("session_id_with_special_chars", "session-123_test.id", {}, {}), ] ) def test_capture_with_session_id_variations( self, test_name, session_id, additional_properties, expected_properties ): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) properties = {"$session_id": session_id, **additional_properties} msg_uuid = client.capture( "python test event", distinct_id="distinct_id", properties=properties ) self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$session_id"], session_id) self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) # Check additional expected properties for key, value in expected_properties.items(): self.assertEqual(msg["properties"][key], value) def test_session_id_preserved_with_groups(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) session_id = "group-session-101" msg_uuid = client.capture( "test_event", distinct_id="distinct_id", properties={"$session_id": session_id}, groups={"company": "id:5", "instance": "app.posthog.com"}, ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["properties"]["$session_id"], session_id) self.assertEqual( msg["properties"]["$groups"], {"company": "id:5", "instance": "app.posthog.com"}, ) def test_session_id_with_anonymous_event(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) session_id = "anonymous-session-202" msg_uuid = client.capture( "anonymous_event", distinct_id="distinct_id", properties={ "$session_id": session_id, "$process_person_profile": False, }, ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["properties"]["$session_id"], session_id) self.assertEqual(msg["properties"]["$process_person_profile"], False) @parameterized.expand( [ # test_name, event_name, session_id, additional_properties, expected_additional_properties ( "screen_event", "$screen", "special-session-505", {"$screen_name": "HomeScreen"}, {"$screen_name": "HomeScreen"}, ), ( "survey_event", "survey sent", "survey-session-606", { "$survey_id": "survey_123", "$survey_questions": [ {"id": "q1", "question": "How likely are you to recommend us?"} ], }, {"$survey_id": "survey_123"}, ), ( "complex_properties_event", "complex_event", "mixed-session-707", { "$current_url": "https://example.com/page", "$process_person_profile": True, "custom_property": "custom_value", "numeric_property": 42, "boolean_property": True, }, { "$current_url": "https://example.com/page", "$process_person_profile": True, "custom_property": "custom_value", "numeric_property": 42, "boolean_property": True, }, ), ( "csp_violation", "$csp_violation", "csp-session-789", { "$csp_version": "1.0", "$current_url": "https://example.com/page", "$process_person_profile": False, "$raw_user_agent": "Mozilla/5.0 Test Agent", "$csp_document_url": "https://example.com/page", "$csp_blocked_url": "https://malicious.com/script.js", "$csp_violated_directive": "script-src", }, { "$csp_version": "1.0", "$current_url": "https://example.com/page", "$process_person_profile": False, "$raw_user_agent": "Mozilla/5.0 Test Agent", "$csp_document_url": "https://example.com/page", "$csp_blocked_url": "https://malicious.com/script.js", "$csp_violated_directive": "script-src", }, ), ] ) def test_session_id_with_different_event_types( self, test_name, event_name, session_id, additional_properties, expected_additional_properties, ): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) properties = {"$session_id": session_id, **additional_properties} msg_uuid = client.capture( event_name, distinct_id="distinct_id", properties=properties ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], event_name) self.assertEqual(msg["properties"]["$session_id"], session_id) # Check additional expected properties for key, value in expected_additional_properties.items(): self.assertEqual(msg["properties"][key], value) # Verify system properties are still added self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) @parameterized.expand( [ # test_name, super_properties, event_session_id, expected_session_id, expected_super_props ( "super_properties_override_session_id", {"$session_id": "super-session", "source": "test"}, "event-session-808", "super-session", {"source": "test"}, ), ( "no_super_properties_conflict", {"source": "test", "version": "1.0"}, "event-session-909", "event-session-909", {"source": "test", "version": "1.0"}, ), ( "empty_super_properties", {}, "event-session-111", "event-session-111", {}, ), ( "super_properties_with_other_dollar_props", {"$current_url": "https://super.com", "source": "test"}, "event-session-222", "event-session-222", {"$current_url": "https://super.com", "source": "test"}, ), ] ) def test_session_id_with_super_properties_variations( self, test_name, super_properties, event_session_id, expected_session_id, expected_super_props, ): with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, super_properties=super_properties, sync_mode=True ) msg_uuid = client.capture( "test_event", distinct_id="distinct_id", properties={"$session_id": event_session_id}, ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["properties"]["$session_id"], expected_session_id) # Check expected super properties are present for key, value in expected_super_props.items(): self.assertEqual(msg["properties"][key], value) def test_flush(self): client = self.client # set up the consumer with more requests than a single batch will allow for i in range(1000): client.capture( "event", distinct_id="distinct_id", properties={"trait": "value"} ) # We can't reliably assert that the queue is non-empty here; that's # a race condition. We do our best to load it up though. client.flush() # Make sure that the client queue is empty after flushing self.assertTrue(client.queue.empty()) def test_shutdown(self): client = self.client # set up the consumer with more requests than a single batch will allow for i in range(1000): client.capture( "test event", distinct_id="distinct_id", properties={"trait": "value"} ) client.shutdown() # we expect two things after shutdown: # 1. client queue is empty # 2. consumer thread has stopped self.assertTrue(client.queue.empty()) for consumer in client.consumers: self.assertFalse(consumer.is_alive()) def test_synchronous(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, sync_mode=True) msg_uuid = client.capture("test event", distinct_id="distinct_id") self.assertFalse(client.consumers) self.assertTrue(client.queue.empty()) self.assertIsNotNone(msg_uuid) # Verify the message was sent immediately mock_post.assert_called_once() def test_overflow(self): client = Client(FAKE_TEST_API_KEY, max_queue_size=1) # Ensure consumer thread is no longer uploading client.join() for i in range(10): client.capture("test event", distinct_id="distinct_id") msg_uuid = client.capture("test event", distinct_id="distinct_id") # Make sure we are informed that the queue is at capacity self.assertIsNone(msg_uuid) def test_unicode(self): Client(six.u("unicode_key")) def test_numeric_distinct_id(self): self.client.capture("python event", distinct_id=1234) self.client.flush() self.assertFalse(self.failed) def test_debug(self): Client("bad_key", debug=True) def test_gzip(self): client = Client(FAKE_TEST_API_KEY, on_error=self.fail, gzip=True) for _ in range(10): client.capture( "event", distinct_id="distinct_id", properties={"trait": "value"} ) client.flush() self.assertFalse(self.failed) def test_user_defined_flush_at(self): client = Client( FAKE_TEST_API_KEY, on_error=self.fail, flush_at=10, flush_interval=3 ) def mock_post_fn(*args, **kwargs): self.assertEqual(len(kwargs["batch"]), 10) # the post function should be called 2 times, with a batch size of 10 # each time. with mock.patch( "posthog.consumer.batch_post", side_effect=mock_post_fn ) as mock_post: for _ in range(20): client.capture( "event", distinct_id="distinct_id", properties={"trait": "value"} ) time.sleep(1) self.assertEqual(mock_post.call_count, 2) def test_user_defined_timeout(self): client = Client(FAKE_TEST_API_KEY, timeout=10) for consumer in client.consumers: self.assertEqual(consumer.timeout, 10) def test_default_timeout_15(self): client = Client(FAKE_TEST_API_KEY) for consumer in client.consumers: self.assertEqual(consumer.timeout, 15) def test_disabled(self): client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, disabled=True) msg_uuid = client.capture("python test event", distinct_id="distinct_id") client.flush() self.assertIsNone(msg_uuid) self.assertFalse(self.failed) @mock.patch("posthog.client.flags") def test_disabled_with_feature_flags(self, patch_flags): client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, disabled=True) response = client.get_feature_flag("beta-feature", "12345") self.assertIsNone(response) patch_flags.assert_not_called() response = client.feature_enabled("beta-feature", "12345") self.assertIsNone(response) patch_flags.assert_not_called() response = client.get_all_flags("12345") self.assertIsNone(response) patch_flags.assert_not_called() response = client.get_feature_flag_payload("key", "12345") self.assertIsNone(response) patch_flags.assert_not_called() response = client.get_all_flags_and_payloads("12345") self.assertEqual(response, {"featureFlags": None, "featureFlagPayloads": None}) patch_flags.assert_not_called() # no capture calls self.assertTrue(client.queue.empty()) def test_enabled_to_disabled(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, disabled=False, sync_mode=True, ) msg_uuid = client.capture("python test event", distinct_id="distinct_id") self.assertIsNotNone(msg_uuid) self.assertFalse(self.failed) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual(msg["event"], "python test event") client.disabled = True msg_uuid = client.capture("python test event", distinct_id="distinct_id") self.assertIsNone(msg_uuid) self.assertFalse(self.failed) def test_disable_geoip_default_on_events(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, disable_geoip=True, sync_mode=True, ) msg_uuid = client.capture("python test event", distinct_id="distinct_id") self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] capture_msg = batch_data[0] self.assertEqual(capture_msg["properties"]["$geoip_disable"], True) def test_disable_geoip_override_on_events(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, disable_geoip=False, sync_mode=True, ) msg_uuid = client.set( distinct_id="distinct_id", properties={"a": "b", "c": "d"}, disable_geoip=True, ) self.assertIsNotNone(msg_uuid) msg_uuid = client.capture( "event", distinct_id="distinct_id", properties={"trait": "value"}, disable_geoip=False, ) self.assertIsNotNone(msg_uuid) # Check both calls were made self.assertEqual(mock_post.call_count, 2) # Check set event set_batch = mock_post.call_args_list[0][1]["batch"] capture_msg = set_batch[0] self.assertEqual(capture_msg["properties"]["$geoip_disable"], True) # Check page event page_batch = mock_post.call_args_list[1][1]["batch"] identify_msg = page_batch[0] self.assertEqual("$geoip_disable" not in identify_msg["properties"], True) def test_disable_geoip_method_overrides_init_on_events(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client( FAKE_TEST_API_KEY, on_error=self.set_fail, disable_geoip=True, sync_mode=True, ) msg_uuid = client.capture( "python test event", distinct_id="distinct_id", disable_geoip=False ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertTrue("$geoip_disable" not in msg["properties"]) @mock.patch("posthog.client.flags") def test_disable_geoip_default_on_decide(self, patch_flags): patch_flags.return_value = { "featureFlags": { "beta-feature": "random-variant", "alpha-feature": True, "off-feature": False, } } client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, disable_geoip=False) client.get_feature_flag("random_key", "some_id", disable_geoip=True) patch_flags.assert_called_with( "random_key", "https://us.i.posthog.com", timeout=3, distinct_id="some_id", groups={}, person_properties={"distinct_id": "some_id"}, group_properties={}, geoip_disable=True, ) patch_flags.reset_mock() client.feature_enabled( "random_key", "feature_enabled_distinct_id", disable_geoip=True ) patch_flags.assert_called_with( "random_key", "https://us.i.posthog.com", timeout=3, distinct_id="feature_enabled_distinct_id", groups={}, person_properties={"distinct_id": "feature_enabled_distinct_id"}, group_properties={}, geoip_disable=True, ) patch_flags.reset_mock() client.get_all_flags_and_payloads("all_flags_payloads_id") patch_flags.assert_called_with( "random_key", "https://us.i.posthog.com", timeout=3, distinct_id="all_flags_payloads_id", groups={}, person_properties={"distinct_id": "all_flags_payloads_id"}, group_properties={}, geoip_disable=False, ) @mock.patch("posthog.client.Poller") @mock.patch("posthog.client.get") def test_call_identify_fails(self, patch_get, patch_poller): def raise_effect(): raise Exception("http exception") patch_get.return_value.raiseError.side_effect = raise_effect client = Client(FAKE_TEST_API_KEY, personal_api_key="test") client.feature_flags = [{"key": "example"}] self.assertFalse(client.feature_enabled("example", "distinct_id")) @mock.patch("posthog.client.flags") def test_default_properties_get_added_properly(self, patch_flags): patch_flags.return_value = { "featureFlags": { "beta-feature": "random-variant", "alpha-feature": True, "off-feature": False, } } client = Client( FAKE_TEST_API_KEY, host="http://app2.posthog.com", on_error=self.set_fail, disable_geoip=False, ) client.get_feature_flag( "random_key", "some_id", groups={"company": "id:5", "instance": "app.posthog.com"}, person_properties={"x1": "y1"}, group_properties={"company": {"x": "y"}}, ) patch_flags.assert_called_with( "random_key", "http://app2.posthog.com", timeout=3, distinct_id="some_id", groups={"company": "id:5", "instance": "app.posthog.com"}, person_properties={"distinct_id": "some_id", "x1": "y1"}, group_properties={ "company": {"$group_key": "id:5", "x": "y"}, "instance": {"$group_key": "app.posthog.com"}, }, geoip_disable=False, ) patch_flags.reset_mock() client.get_feature_flag( "random_key", "some_id", groups={"company": "id:5", "instance": "app.posthog.com"}, person_properties={"distinct_id": "override"}, group_properties={ "company": { "$group_key": "group_override", } }, ) patch_flags.assert_called_with( "random_key", "http://app2.posthog.com", timeout=3, distinct_id="some_id", groups={"company": "id:5", "instance": "app.posthog.com"}, person_properties={"distinct_id": "override"}, group_properties={ "company": {"$group_key": "group_override"}, "instance": {"$group_key": "app.posthog.com"}, }, geoip_disable=False, ) patch_flags.reset_mock() # test nones client.get_all_flags_and_payloads( "some_id", groups={}, person_properties=None, group_properties=None ) patch_flags.assert_called_with( "random_key", "http://app2.posthog.com", timeout=3, distinct_id="some_id", groups={}, person_properties={"distinct_id": "some_id"}, group_properties={}, geoip_disable=False, ) @parameterized.expand( [ # name, sys_platform, version_info, expected_runtime, expected_version, expected_os, expected_os_version, platform_method, platform_return, distro_info ( "macOS", "darwin", (3, 8, 10), "MockPython", "3.8.10", "Mac OS X", "10.15.7", "mac_ver", ("10.15.7", "", ""), None, ), ( "Windows", "win32", (3, 8, 10), "MockPython", "3.8.10", "Windows", "10", "win32_ver", ("10", "", "", ""), None, ), ( "Linux", "linux", (3, 8, 10), "MockPython", "3.8.10", "Linux", "20.04", None, None, {"version": "20.04"}, ), ] ) def test_mock_system_context( self, _name, sys_platform, version_info, expected_runtime, expected_version, expected_os, expected_os_version, platform_method, platform_return, distro_info, ): """Test that we can mock platform and sys for testing system_context""" with mock.patch("posthog.utils.platform") as mock_platform: with mock.patch("posthog.utils.sys") as mock_sys: # Set up common mocks mock_platform.python_implementation.return_value = expected_runtime mock_sys.version_info = version_info mock_sys.platform = sys_platform # Set up platform-specific mocks if platform_method: getattr( mock_platform, platform_method ).return_value = platform_return # Special handling for Linux which uses distro module if sys_platform == "linux": # Directly patch the get_os_info function to return our expected values with mock.patch( "posthog.utils.get_os_info", return_value=(expected_os, expected_os_version), ): from posthog.utils import system_context context = system_context() else: # Get system context for non-Linux platforms from posthog.utils import system_context context = system_context() # Verify results expected_context = { "$python_runtime": expected_runtime, "$python_version": expected_version, "$os": expected_os, "$os_version": expected_os_version, } assert context == expected_context @mock.patch("posthog.client.flags") def test_get_decide_returns_normalized_decide_response(self, patch_flags): patch_flags.return_value = { "featureFlags": { "beta-feature": "random-variant", "alpha-feature": True, "off-feature": False, }, "featureFlagPayloads": {"beta-feature": '{"some": "data"}'}, "errorsWhileComputingFlags": False, "requestId": "test-id", } client = Client(FAKE_TEST_API_KEY) distinct_id = "test_distinct_id" groups = {"test_group_type": "test_group_id"} person_properties = {"test_property": "test_value"} response = client.get_flags_decision(distinct_id, groups, person_properties) assert response == { "flags": { "beta-feature": FeatureFlag( key="beta-feature", enabled=True, variant="random-variant", reason=None, metadata=LegacyFlagMetadata( payload='{"some": "data"}', ), ), "alpha-feature": FeatureFlag( key="alpha-feature", enabled=True, variant=None, reason=None, metadata=LegacyFlagMetadata( payload=None, ), ), "off-feature": FeatureFlag( key="off-feature", enabled=False, variant=None, reason=None, metadata=LegacyFlagMetadata( payload=None, ), ), }, "errorsWhileComputingFlags": False, "requestId": "test-id", } def test_set_context_session_with_capture(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) with new_context(): set_context_session("context-session-123") msg_uuid = client.capture( "test_event", distinct_id="distinct_id", properties={"custom_prop": "value"}, ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual( msg["properties"]["$session_id"], "context-session-123" ) def test_set_context_session_with_page_explicit_properties(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) with new_context(): set_context_session("page-explicit-session-789") properties = { "$session_id": get_context_session_id(), "page_type": "landing", } msg_uuid = client.capture( "$page", distinct_id="distinct_id", properties=properties ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual( msg["properties"]["$session_id"], "page-explicit-session-789" ) def test_set_context_session_override_in_capture(self): """Test that explicit session ID overrides context session ID in capture""" from posthog.contexts import new_context, set_context_session with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) with new_context(): set_context_session("context-session-override") msg_uuid = client.capture( "test_event", distinct_id="distinct_id", properties={ "$session_id": "explicit-session-override", "custom_prop": "value", }, ) self.assertIsNotNone(msg_uuid) # Get the enqueued message from the mock mock_post.assert_called_once() batch_data = mock_post.call_args[1]["batch"] msg = batch_data[0] self.assertEqual( msg["properties"]["$session_id"], "explicit-session-override" ) @mock.patch("posthog.client.Poller") @mock.patch("posthog.client.get") def test_enable_local_evaluation_false_disables_poller( self, patch_get, patch_poller ): """Test that when enable_local_evaluation=False, the poller is not started""" patch_get.return_value = { "flags": [ {"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True} ], "group_type_mapping": {}, "cohorts": {}, } client = Client( FAKE_TEST_API_KEY, personal_api_key="test-personal-key", enable_local_evaluation=False, ) # Load feature flags should not start the poller client.load_feature_flags() # Assert that the poller was not created/started patch_poller.assert_not_called() # But the feature flags should still be loaded patch_get.assert_called_once() self.assertEqual(len(client.feature_flags), 1) self.assertEqual(client.feature_flags[0]["key"], "beta-feature") @mock.patch("posthog.client.Poller") @mock.patch("posthog.client.get") def test_enable_local_evaluation_true_starts_poller(self, patch_get, patch_poller): """Test that when enable_local_evaluation=True (default), the poller is started""" patch_get.return_value = { "flags": [ {"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True} ], "group_type_mapping": {}, "cohorts": {}, } client = Client( FAKE_TEST_API_KEY, personal_api_key="test-personal-key", enable_local_evaluation=True, ) # Load feature flags should start the poller client.load_feature_flags() # Assert that the poller was created and started patch_poller.assert_called_once() patch_get.assert_called_once() self.assertEqual(len(client.feature_flags), 1) self.assertEqual(client.feature_flags[0]["key"], "beta-feature") @mock.patch("posthog.client.remote_config") def test_get_remote_config_payload_works_without_poller(self, patch_remote_config): """Test that get_remote_config_payload works without local evaluation enabled""" patch_remote_config.return_value = {"test": "payload"} client = Client( FAKE_TEST_API_KEY, personal_api_key="test-personal-key", enable_local_evaluation=False, ) # Should work without poller result = client.get_remote_config_payload("test-flag") self.assertEqual(result, {"test": "payload"}) patch_remote_config.assert_called_once_with( "test-personal-key", FAKE_TEST_API_KEY, client.host, "test-flag", timeout=client.feature_flags_request_timeout_seconds, ) def test_get_remote_config_payload_requires_personal_api_key(self): """Test that get_remote_config_payload requires personal API key""" client = Client( FAKE_TEST_API_KEY, enable_local_evaluation=False, ) result = client.get_remote_config_payload("test-flag") self.assertIsNone(result) def test_parse_send_feature_flags_method(self): """Test the _parse_send_feature_flags helper method""" client = Client(FAKE_TEST_API_KEY, sync_mode=True) # Test boolean True result = client._parse_send_feature_flags(True) expected = { "should_send": True, "only_evaluate_locally": None, "person_properties": None, "group_properties": None, } self.assertEqual(result, expected) # Test boolean False result = client._parse_send_feature_flags(False) expected = { "should_send": False, "only_evaluate_locally": None, "person_properties": None, "group_properties": None, } self.assertEqual(result, expected) # Test options dict with all fields options = { "only_evaluate_locally": True, "person_properties": {"plan": "premium"}, "group_properties": {"company": {"type": "enterprise"}}, } result = client._parse_send_feature_flags(options) expected = { "should_send": True, "only_evaluate_locally": True, "person_properties": {"plan": "premium"}, "group_properties": {"company": {"type": "enterprise"}}, } self.assertEqual(result, expected) # Test options dict with partial fields options = {"person_properties": {"user_id": "123"}} result = client._parse_send_feature_flags(options) expected = { "should_send": True, "only_evaluate_locally": None, "person_properties": {"user_id": "123"}, "group_properties": None, } self.assertEqual(result, expected) # Test empty dict result = client._parse_send_feature_flags({}) expected = { "should_send": True, "only_evaluate_locally": None, "person_properties": None, "group_properties": None, } self.assertEqual(result, expected) # Test invalid types with self.assertRaises(TypeError) as cm: client._parse_send_feature_flags("invalid") self.assertIn("Invalid type for send_feature_flags", str(cm.exception)) with self.assertRaises(TypeError) as cm: client._parse_send_feature_flags(123) self.assertIn("Invalid type for send_feature_flags", str(cm.exception)) with self.assertRaises(TypeError) as cm: client._parse_send_feature_flags(None) self.assertIn("Invalid type for send_feature_flags", str(cm.exception)) @mock.patch("posthog.client.batch_post") def test_get_feature_flag_result_with_empty_string_payload(self, patch_batch_post): """Test that get_feature_flag_result returns a FeatureFlagResult when payload is empty string""" client = Client( FAKE_TEST_API_KEY, personal_api_key="test_personal_api_key", sync_mode=True, ) # Set up local evaluation with a flag that has empty string payload client.feature_flags = [ { "id": 1, "name": "Test flag", "key": "test-flag", "is_simple_flag": False, "active": True, "rollout_percentage": None, "filters": { "groups": [ { "properties": [], "rollout_percentage": None, "variant": "empty-variant", } ], "multivariate": { "variants": [ { "key": "empty-variant", "name": "Empty Variant", "rollout_percentage": 100, } ] }, "payloads": {"empty-variant": ""}, # Empty string payload }, } ] # Test get_feature_flag_result result = client.get_feature_flag_result( "test-flag", "test-user", only_evaluate_locally=True ) # Should return a FeatureFlagResult, not None self.assertIsNotNone(result) self.assertEqual(result.key, "test-flag") self.assertEqual(result.get_value(), "empty-variant") self.assertEqual(result.payload, "") # Should be empty string, not None @mock.patch("posthog.client.batch_post") def test_get_all_flags_and_payloads_with_empty_string(self, patch_batch_post): """Test that get_all_flags_and_payloads includes flags with empty string payloads""" client = Client( FAKE_TEST_API_KEY, personal_api_key="test_personal_api_key", sync_mode=True, ) # Set up multiple flags with different payload types client.feature_flags = [ { "id": 1, "name": "Flag with empty payload", "key": "empty-payload-flag", "is_simple_flag": False, "active": True, "filters": { "groups": [{"properties": [], "variant": "variant1"}], "multivariate": { "variants": [{"key": "variant1", "rollout_percentage": 100}] }, "payloads": {"variant1": ""}, # Empty string }, }, { "id": 2, "name": "Flag with normal payload", "key": "normal-payload-flag", "is_simple_flag": False, "active": True, "filters": { "groups": [{"properties": [], "variant": "variant2"}], "multivariate": { "variants": [{"key": "variant2", "rollout_percentage": 100}] }, "payloads": {"variant2": "normal payload"}, }, }, ] result = client.get_all_flags_and_payloads( "test-user", only_evaluate_locally=True ) # Check that both flags are included self.assertEqual(result["featureFlags"]["empty-payload-flag"], "variant1") self.assertEqual(result["featureFlags"]["normal-payload-flag"], "variant2") # Check that empty string payload is included (not filtered out) self.assertIn("empty-payload-flag", result["featureFlagPayloads"]) self.assertEqual(result["featureFlagPayloads"]["empty-payload-flag"], "") self.assertEqual( result["featureFlagPayloads"]["normal-payload-flag"], "normal payload" )