From 53cddbb6a376d95d1906f282634d0635285fd17d Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 30 Apr 2026 10:29:02 +0200 Subject: [PATCH] fix: no-op clients without api key --- .sampo/changesets/disabled-no-api-key.md | 5 +++ posthog/__init__.py | 5 +-- posthog/client.py | 11 +++++-- posthog/test/test_client.py | 39 ++++++++++++++++++++++-- posthog/test/test_module.py | 25 +++++++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 .sampo/changesets/disabled-no-api-key.md diff --git a/.sampo/changesets/disabled-no-api-key.md b/.sampo/changesets/disabled-no-api-key.md new file mode 100644 index 00000000..27287ea8 --- /dev/null +++ b/.sampo/changesets/disabled-no-api-key.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Treat clients with an empty project API key as disabled no-ops. diff --git a/posthog/__init__.py b/posthog/__init__.py index b594c01a..0a3700f7 100644 --- a/posthog/__init__.py +++ b/posthog/__init__.py @@ -886,8 +886,9 @@ def setup() -> Client: in_app_modules=in_app_modules, ) - # always set incase user changes it - default_client.disabled = disabled + # Always set in case user changes it. Preserve Client's auto-disabled state + # for API keys that become empty after trimming. + default_client.disabled = disabled or not default_client.api_key default_client.debug = debug default_client._set_before_send(before_send) diff --git a/posthog/client.py b/posthog/client.py index a4ffa1ed..998f88d1 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -220,7 +220,7 @@ def __init__( self.queue = queue.Queue(max_queue_size) # api_key: This should be the Team API Key (token), public - self.api_key = project_api_key.strip() + self.api_key = (project_api_key or "").strip() self.on_error = on_error self.debug = debug @@ -245,7 +245,7 @@ def __init__( self.flag_definition_version = 0 self._flags_etag: Optional[str] = None self._flag_definition_cache_provider = flag_definition_cache_provider - self.disabled = disabled + self.disabled = disabled or not self.api_key self.disable_geoip = disable_geoip self.historical_migration = historical_migration self.super_properties = super_properties @@ -538,6 +538,9 @@ def get_flags_decision( Category: Feature flags """ + if self.disabled: + return normalize_flags_response({}) + groups = groups or {} person_properties = person_properties or {} group_properties = group_properties or {} @@ -1408,6 +1411,10 @@ def load_feature_flags(self): Category: Feature flags """ + if self.disabled: + self.feature_flags = [] + return + if not self.personal_api_key: self.log.warning( "[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags." diff --git a/posthog/test/test_client.py b/posthog/test/test_client.py index 587e7005..db0e6b1a 100644 --- a/posthog/test/test_client.py +++ b/posthog/test/test_client.py @@ -43,17 +43,19 @@ def test_requires_api_key(self): @parameterized.expand( [ - ("valid_key", " \nphc_validkey\t ", "phc_validkey", False), - ("whitespace_only", " \n\t ", "", True), + ("valid_key", " \nphc_validkey\t ", "phc_validkey", False, False), + ("whitespace_only", " \n\t ", "", True, True), + ("empty_string", "", "", True, True), ] ) def test_trims_api_key_whitespace( - self, _, raw_api_key, expected_api_key, expect_error_log + self, _, raw_api_key, expected_api_key, expected_disabled, expect_error_log ): with mock.patch.object(Client.log, "error") as mock_error: client = Client(raw_api_key, send=False) self.assertEqual(client.api_key, expected_api_key) + self.assertEqual(client.disabled, expected_disabled) if expect_error_log: mock_error.assert_called_once_with( "api_key is empty after trimming whitespace; check your project API key" @@ -73,6 +75,37 @@ def test_trims_host_and_personal_api_key_whitespace(self): self.assertEqual(client.host, "https://eu.i.posthog.com") self.assertIsNone(client.personal_api_key) + def test_client_with_empty_api_key_is_noop(self): + client = Client("", send=False) + + self.assertIsNone(client.capture("event", distinct_id="distinct_id")) + + @mock.patch("posthog.client.get") + def test_disabled_client_does_not_load_feature_flags(self, patch_get): + client = Client("", personal_api_key="test", send=False) + + client.load_feature_flags() + + patch_get.assert_not_called() + self.assertEqual(client.feature_flags, []) + self.assertIsNone(client.poller) + + @mock.patch("posthog.client.flags") + def test_disabled_client_does_not_get_flags_decision(self, patch_flags): + client = Client("", send=False) + + self.assertEqual(client.get_flags_decision("distinct_id")["flags"], {}) + self.assertEqual(client.get_feature_variants("distinct_id"), {}) + self.assertEqual(client.get_feature_payloads("distinct_id"), {}) + self.assertEqual( + client.get_feature_flags_and_payloads("distinct_id"), + {"featureFlags": {}, "featureFlagPayloads": {}}, + ) + self.assertIsNone( + client.capture("event", distinct_id="distinct_id", send_feature_flags=True) + ) + patch_flags.assert_not_called() + def test_empty_flush(self): self.client.flush() diff --git a/posthog/test/test_module.py b/posthog/test/test_module.py index 03bce00b..aa87314f 100644 --- a/posthog/test/test_module.py +++ b/posthog/test/test_module.py @@ -36,6 +36,31 @@ def test_flush(self): self.posthog.flush() +class TestModuleLevelSetup(unittest.TestCase): + def setUp(self): + self._original_default_client = posthog.default_client + self._original_api_key = posthog.api_key + self._original_disabled = posthog.disabled + self._original_send = posthog.send + posthog.default_client = None + posthog.api_key = " \n\t " + posthog.disabled = False + posthog.send = False + + def tearDown(self): + posthog.default_client = self._original_default_client + posthog.api_key = self._original_api_key + posthog.disabled = self._original_disabled + posthog.send = self._original_send + + def test_setup_preserves_client_disabled_when_trimmed_api_key_is_empty(self): + posthog.setup() + + self.assertIsNotNone(posthog.default_client) + self.assertEqual(posthog.default_client.api_key, "") + self.assertTrue(posthog.default_client.disabled) + + class TestModuleLevelWrappers(unittest.TestCase): """Test that module-level wrapper functions in posthog/__init__.py correctly propagate all parameters to the Client methods."""