Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,9 @@ def get_tags() -> Dict[str, Any]:
sync_mode: If True, send events synchronously instead of using background
worker threads.
disabled: If True, disable captures and API requests. Useful in tests.
personal_api_key: Personal API key used for local feature flag evaluation
and remote config payloads.
secret_key: A Personal API Key or Project Secret API Key used for local
feature flag evaluation and remote config payloads.
personal_api_key: Deprecated alias for secret_key.
poll_interval: Seconds between local feature flag definition refreshes.
disable_geoip: Whether to disable server-side GeoIP enrichment. Defaults to
True.
Expand Down Expand Up @@ -362,7 +363,8 @@ def get_tags() -> Dict[str, Any]:
send = True # type: bool
sync_mode = False # type: bool
disabled = False # type: bool
personal_api_key = None # type: Optional[str]
secret_key = None # type: Optional[str]
personal_api_key = None # type: Optional[str] # Deprecated: use secret_key
project_api_key = None # type: Optional[str]
poll_interval = 30 # type: int
disable_geoip = True # type: bool
Expand Down Expand Up @@ -930,7 +932,7 @@ def get_remote_config_payload(
The payload associated with the feature flag. If payload is encrypted, the return value will be decrypted

Note:
Requires personal_api_key to be set for authentication
Requires secret_key to be set for authentication
"""
return _proxy(
"get_remote_config_payload",
Expand Down Expand Up @@ -1154,6 +1156,7 @@ def setup() -> Client:
on_error=on_error,
send=send,
sync_mode=sync_mode,
secret_key=secret_key,
personal_api_key=personal_api_key,
poll_interval=poll_interval,
disabled=disabled,
Expand Down
45 changes: 33 additions & 12 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ def __init__(
timeout=15,
thread=1,
poll_interval=30,
secret_key=None,
personal_api_key=None,
disabled=False,
disable_geoip=True,
Expand Down Expand Up @@ -286,8 +287,15 @@ def __init__(
timeout: HTTP request timeout in seconds for event uploads.
thread: Number of background consumer threads.
poll_interval: Seconds between local feature flag definition refreshes.
personal_api_key: Personal API key used for local feature flag
evaluation and remote config payloads.
secret_key: A Personal API Key or Project Secret API Key, used to
authenticate local feature flag evaluation, remote config
payloads, and decrypted flag payloads. Example::

posthog.Client(project_api_key, secret_key="phx_...")

personal_api_key: Deprecated alias for ``secret_key``. Still honored
for backwards compatibility; prefer ``secret_key``, which also
accepts a Project Secret API Key.
disabled: If True, disable captures and API requests. Useful in tests.
disable_geoip: Whether to disable server-side GeoIP enrichment.
Defaults to True.
Expand Down Expand Up @@ -447,12 +455,25 @@ def __init__(

self.project_root = project_root

# personal_api_key: This should be a generated Personal API Key, private
self.personal_api_key = (
personal_api_key.strip()
if isinstance(personal_api_key, str)
else personal_api_key
if personal_api_key is not None and secret_key is None:
warnings.warn(
"`personal_api_key` is deprecated; use `secret_key` instead. "
"`secret_key` accepts a Personal API Key or a Project Secret API Key.",
DeprecationWarning,
stacklevel=2,
)
elif secret_key is not None and personal_api_key is not None:
self.log.warning(
"[FEATURE FLAGS] Both `secret_key` and `personal_api_key` were "
"provided; using `secret_key` and ignoring `personal_api_key`."
)
resolved_secret_key = secret_key if secret_key is not None else personal_api_key
self.secret_key = (
resolved_secret_key.strip()
if isinstance(resolved_secret_key, str)
else resolved_secret_key
) or None
self.personal_api_key = self.secret_key
if debug:
# Ensures that debug level messages are logged when debug mode is on.
# Otherwise, defaults to WARNING level. See https://docs.python.org/3/howto/logging.html#what-happens-if-no-configuration-is-provided
Expand Down Expand Up @@ -1824,7 +1845,7 @@ def _fetch_feature_flags_from_api(self):
personal_api_key = self.personal_api_key
if personal_api_key is None:
self.log.warning(
"[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags."
"[FEATURE FLAGS] You have to specify a secret_key to use feature flags."
)
return

Expand Down Expand Up @@ -1879,7 +1900,7 @@ def _fetch_feature_flags_from_api(self):
if e.status == 401:
detail = (
f"Error loading feature flags: {e.message}. "
"Please verify both your project_api_key and personal_api_key. "
"Please verify both your project_api_key and secret_key. "
"More information: https://posthog.com/docs/api/overview"
)
self.log.error("[FEATURE FLAGS] %s", detail)
Expand Down Expand Up @@ -1939,7 +1960,7 @@ def load_feature_flags(self):

if not self.personal_api_key:
self.log.warning(
"[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags."
"[FEATURE FLAGS] You have to specify a secret_key to use feature flags."
)
self.feature_flags = []
return
Expand Down Expand Up @@ -2633,7 +2654,7 @@ def get_remote_config_payload(self, key: str):
returned.

Note:
Requires ``personal_api_key`` for authentication.
Requires ``secret_key`` for authentication.

Category:
Feature flags
Expand All @@ -2643,7 +2664,7 @@ def get_remote_config_payload(self, key: str):

if self.personal_api_key is None:
self.log.warning(
"[FEATURE FLAGS] You have to specify a personal_api_key to fetch decrypted feature flag payloads."
"[FEATURE FLAGS] You have to specify a secret_key to fetch decrypted feature flag payloads."
)
return None

Expand Down
27 changes: 26 additions & 1 deletion posthog/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import asyncio
import time
import unittest
import warnings
from datetime import datetime
from uuid import UUID, uuid4

Expand Down Expand Up @@ -78,6 +79,30 @@ 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)

@parameterized.expand(
[
("secret_key_only", "phx_secret", None, "phx_secret", False),
("personal_api_key_alias", None, "phx_legacy", "phx_legacy", True),
("secret_key_wins", "phx_secret", "phx_legacy", "phx_secret", False),
]
)
def test_secret_key_resolution(
self, _, secret_key, personal_api_key, expected, expect_deprecation
):
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
client = Client(
FAKE_TEST_API_KEY,
secret_key=secret_key,
personal_api_key=personal_api_key,
send=False,
)

self.assertEqual(client.secret_key, expected)
self.assertEqual(client.personal_api_key, expected)
deprecations = [w for w in caught if issubclass(w.category, DeprecationWarning)]
self.assertEqual(bool(deprecations), expect_deprecation)

def test_client_with_empty_api_key_is_noop(self):
client = Client("", send=False)

Expand Down Expand Up @@ -810,7 +835,7 @@ def test_load_feature_flags_unauthorized(self, patch_get):
self.assertEqual(client.cohorts, {})
self.assertIn("Unauthorized", logs.output[0])
self.assertIn("project_api_key", logs.output[0])
self.assertIn("personal_api_key", logs.output[0])
self.assertIn("secret_key", logs.output[0])

@mock.patch("posthog.client.flags")
def test_dont_override_capture_with_local_flags(self, patch_flags):
Expand Down
2 changes: 1 addition & 1 deletion posthog/test/test_feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -2767,7 +2767,7 @@ def test_load_feature_flags_wrong_key(self, patch_get, _patch_poll):
client.load_feature_flags()
self.assertIn("Unauthorized", logs.output[0])
self.assertIn("project_api_key", logs.output[0])
self.assertIn("personal_api_key", logs.output[0])
self.assertIn("secret_key", logs.output[0])
client.debug = True
with self.assertRaisesRegex(APIError, "Unauthorized"):
client.load_feature_flags()
Expand Down
6 changes: 4 additions & 2 deletions references/public_api_snapshot.txt
Original file line number Diff line number Diff line change
Expand Up @@ -506,13 +506,14 @@ attribute posthog.client.Client.is_server = is_server
attribute posthog.client.Client.log = logging.getLogger('posthog')
attribute posthog.client.Client.log_captured_exceptions = log_captured_exceptions
attribute posthog.client.Client.on_error = on_error
attribute posthog.client.Client.personal_api_key = (personal_api_key.strip() if isinstance(personal_api_key, str) else personal_api_key) or None
attribute posthog.client.Client.personal_api_key = self.secret_key
attribute posthog.client.Client.poll_interval = poll_interval
attribute posthog.client.Client.poller: Optional[Poller] = None
attribute posthog.client.Client.privacy_mode = privacy_mode
attribute posthog.client.Client.project_root = project_root
attribute posthog.client.Client.queue: Queue = Queue(max_queue_size)
attribute posthog.client.Client.raw_host = normalize_host(host)
attribute posthog.client.Client.secret_key = (resolved_secret_key.strip() if isinstance(resolved_secret_key, str) else resolved_secret_key) or None
attribute posthog.client.Client.send = send
attribute posthog.client.Client.super_properties = super_properties
attribute posthog.client.Client.sync_mode = sync_mode
Expand Down Expand Up @@ -709,6 +710,7 @@ attribute posthog.request.RequestsTimeout = requests.exceptions.Timeout
attribute posthog.request.SocketOptions = List[Tuple[int, int, Union[int, bytes]]]
attribute posthog.request.USER_AGENT = 'posthog-python/' + VERSION
attribute posthog.request.US_INGESTION_ENDPOINT = 'https://us.i.posthog.com'
attribute posthog.secret_key = None
attribute posthog.send = True
attribute posthog.super_properties = None
attribute posthog.sync_mode = False
Expand Down Expand Up @@ -827,7 +829,7 @@ class posthog.ai.types.ToolInProgress
class posthog.args.OptionalCaptureArgs
class posthog.args.OptionalSetArgs
class posthog.bucketed_rate_limiter.BucketedRateLimiter(bucket_size: Number, refill_rate: Number, refill_interval_seconds: Number, on_bucket_rate_limited: Optional[Callable[[Hashable], None]] = None, clock: Callable[[], float] = time.monotonic)
class posthog.client.Client(project_api_key: str, host=None, debug=False, max_queue_size=10000, send=True, on_error=None, flush_at=100, flush_interval=5.0, gzip=False, max_retries=3, sync_mode=False, timeout=15, thread=1, poll_interval=30, personal_api_key=None, disabled=False, disable_geoip=True, is_server=True, historical_migration=False, feature_flags_request_timeout_seconds=3, feature_flags_request_max_retries=1, super_properties=None, enable_exception_autocapture=False, log_captured_exceptions=False, project_root=None, privacy_mode=False, before_send=None, flag_fallback_cache_url=None, enable_local_evaluation=True, flag_definition_cache_provider: Optional[FlagDefinitionCacheProvider] = None, capture_exception_code_variables=False, code_variables_mask_patterns=None, code_variables_ignore_patterns=None, code_variables_mask_url_credentials=None, code_variables_detect_secrets=None, in_app_modules: list[str] | None = None, enable_exception_autocapture_rate_limiting=False, exception_autocapture_bucket_size=ExceptionCapture.DEFAULT_BUCKET_SIZE, exception_autocapture_refill_rate=ExceptionCapture.DEFAULT_REFILL_RATE, exception_autocapture_refill_interval_seconds=ExceptionCapture.DEFAULT_REFILL_INTERVAL_SECONDS, _dedicated_ai_endpoint=False)
class posthog.client.Client(project_api_key: str, host=None, debug=False, max_queue_size=10000, send=True, on_error=None, flush_at=100, flush_interval=5.0, gzip=False, max_retries=3, sync_mode=False, timeout=15, thread=1, poll_interval=30, secret_key=None, personal_api_key=None, disabled=False, disable_geoip=True, is_server=True, historical_migration=False, feature_flags_request_timeout_seconds=3, feature_flags_request_max_retries=1, super_properties=None, enable_exception_autocapture=False, log_captured_exceptions=False, project_root=None, privacy_mode=False, before_send=None, flag_fallback_cache_url=None, enable_local_evaluation=True, flag_definition_cache_provider: Optional[FlagDefinitionCacheProvider] = None, capture_exception_code_variables=False, code_variables_mask_patterns=None, code_variables_ignore_patterns=None, code_variables_mask_url_credentials=None, code_variables_detect_secrets=None, in_app_modules: list[str] | None = None, enable_exception_autocapture_rate_limiting=False, exception_autocapture_bucket_size=ExceptionCapture.DEFAULT_BUCKET_SIZE, exception_autocapture_refill_rate=ExceptionCapture.DEFAULT_REFILL_RATE, exception_autocapture_refill_interval_seconds=ExceptionCapture.DEFAULT_REFILL_INTERVAL_SECONDS, _dedicated_ai_endpoint=False)
class posthog.consumer.Consumer(queue, api_key, flush_at=100, host=None, on_error=None, flush_interval=5.0, gzip=False, retries=10, timeout=15, historical_migration=False, dedicated_ai_endpoint=False)
class posthog.contexts.ContextScope(parent=None, fresh: bool = False, capture_exceptions: bool = True, client: Optional[Client] = None)
class posthog.exception_capture.ExceptionCapture(client: Client, rate_limiting_enabled=False, bucket_size=DEFAULT_BUCKET_SIZE, refill_rate=DEFAULT_REFILL_RATE, refill_interval_seconds=DEFAULT_REFILL_INTERVAL_SECONDS)
Expand Down
Loading