Compare commits

...
10 Commits
Author SHA1 Message Date
Phani RajandGitHub 16cbd10f1b bump version to 3.5.2 (#130) 2024-08-21 11:58:02 -05:00
Phani RajandGitHub b83d544931 fix(feature flags): Guard for None values when comparing person Properties (#129)
* Guard for None values when comparing person Properties
2024-08-21 11:18:20 -05:00
Frank HamandandGitHub 72c0ed1935 Switch us-api.i hosts to just us.i (#119)
We dropped the -api as it's a bit confusing (is capture really api?)
2024-08-14 08:41:10 +01:00
Neil KakkarandGitHub 5fdd6177ee Create CODEOWNERS (#122) 2024-05-02 18:02:13 +01:00
Neil KakkarandGitHub fc1da7d589 fix(flags): Add a shorter configurable timeout for flag requests (#120) 2024-03-04 14:25:11 +00:00
Brett HoernerandGitHub cba6e86537 Bump to 3.4.2 (#118) 2024-02-20 08:43:10 -07:00
Brett HoernerandGitHub 4e45255207 Add historical_migration option to toplevel Client (#117) 2024-02-15 06:42:43 -07:00
Neil KakkarandGitHub bc37351ab4 chore: Use ingestion hosts for event capture (#116) 2024-02-13 11:19:21 +00:00
Neil KakkarandGitHub a5e8b7d7fb fix(routing): Update hosts to point to right ingestion host (#115) 2024-02-05 12:14:14 +00:00
Neil KakkarandGitHub efb0ccf3c7 chore(flags): Update type hints for newer mypy versions (#114)
* chore(flags): Update type hints for newer mypy versions

* black
2024-01-30 16:06:10 +00:00
11 changed files with 201 additions and 44 deletions
+29 -1
View File
@@ -1,3 +1,31 @@
## 3.5.2 - 2024-08-21
1. Guard for None values in local evaluation
## 3.5.1 - 2024-08-13
1. Remove "-api" suffix from ingestion hostnames
## 3.5.0 - 2024-02-29
1. - Adds a new `feature_flags_request_timeout_seconds` timeout parameter for feature flags which defaults to 3 seconds, updated from the default 10s for all other API calls.
## 3.4.2 - 2024-02-20
1. Add `historical_migration` option for bulk migration to PostHog Cloud.
## 3.4.1 - 2024-02-09
1. Use new hosts for event capture as well
## 3.4.0 - 2024-02-05
1. Point given hosts to new ingestion hosts
## 3.3.4 - 2024-01-30
1. Update type hints for module variables to work with newer versions of mypy
## 3.3.3 - 2024-01-26
1. Remove new relative date operators, combine into regular date operators
@@ -111,7 +139,7 @@ Changes:
Breaking changes:
1. The minimum version requirement for PostHog servers is now 1.38. If you're using PostHog Cloud, you satisfy this requirement automatically.
2. Feature flag defaults apply only when there's an error fetching feature flag results. Earlier, if the default was set to `True`, even if a flag resolved to `False`, the default would override this.
2. Feature flag defaults apply only when there's an error fetching feature flag results. Earlier, if the default was set to `True`, even if a flag resolved to `False`, the default would override this.
**Note: These are removed in 2.0.2**
3. Feature flag remote evaluation doesn't require a personal API key.
+1
View File
@@ -0,0 +1 @@
@PostHog/team-feature-success
+13 -6
View File
@@ -7,19 +7,20 @@ from posthog.version import VERSION
__version__ = VERSION
"""Settings."""
api_key = None # type: str
host = None # type: str
on_error = None # type: Callable
api_key = None # type: Optional[str]
host = None # type: Optional[str]
on_error = None # type: Optional[Callable]
debug = False # type: bool
send = True # type: bool
sync_mode = False # type: bool
disabled = False # type: bool
personal_api_key = None # type: str
project_api_key = None # type: str
personal_api_key = None # type: Optional[str]
project_api_key = None # type: Optional[str]
poll_interval = 30 # type: int
disable_geoip = True # type: bool
feature_flags_request_timeout_seconds = 3 # type: int
default_client = None
default_client = None # type: Optional[Client]
def capture(
@@ -405,6 +406,11 @@ def feature_flag_definitions():
return _proxy("feature_flag_definitions")
def load_feature_flags():
"""Load feature flag definitions from PostHog."""
return _proxy("load_feature_flags")
def page(*args, **kwargs):
"""Send a page call."""
_proxy("page", *args, **kwargs)
@@ -447,6 +453,7 @@ def _proxy(method, *args, **kwargs):
poll_interval=poll_interval,
disabled=disabled,
disable_geoip=disable_geoip,
feature_flags_request_timeout_seconds=feature_flags_request_timeout_seconds,
)
# always set incase user changes it
+17 -5
View File
@@ -10,7 +10,7 @@ from six import string_types
from posthog.consumer import Consumer
from posthog.feature_flags import InconclusiveMatchError, match_feature_flag_properties
from posthog.poller import Poller
from posthog.request import APIError, batch_post, decide, get
from posthog.request import APIError, batch_post, decide, determine_server_host, get
from posthog.utils import SizeLimitedDict, clean, guess_timezone
from posthog.version import VERSION
@@ -49,6 +49,8 @@ class Client(object):
project_api_key=None,
disabled=False,
disable_geoip=True,
historical_migration=False,
feature_flags_request_timeout_seconds=3,
):
self.queue = queue.Queue(max_queue_size)
@@ -61,7 +63,7 @@ class Client(object):
self.debug = debug
self.send = send
self.sync_mode = sync_mode
self.host = host
self.host = determine_server_host(host)
self.gzip = gzip
self.timeout = timeout
self.feature_flags = None
@@ -69,10 +71,12 @@ class Client(object):
self.group_type_mapping = None
self.cohorts = None
self.poll_interval = poll_interval
self.feature_flags_request_timeout_seconds = feature_flags_request_timeout_seconds
self.poller = None
self.distinct_ids_feature_flags_reported = SizeLimitedDict(MAX_DICT_SIZE, set)
self.disabled = disabled
self.disable_geoip = disable_geoip
self.historical_migration = historical_migration
# personal_api_key: This should be a generated Personal API Key, private
self.personal_api_key = personal_api_key
@@ -100,13 +104,14 @@ class Client(object):
consumer = Consumer(
self.queue,
self.api_key,
host=host,
host=self.host,
on_error=on_error,
flush_at=flush_at,
flush_interval=flush_interval,
gzip=gzip,
retries=max_retries,
timeout=timeout,
historical_migration=historical_migration,
)
self.consumers.append(consumer)
@@ -161,7 +166,7 @@ class Client(object):
"group_properties": group_properties,
"disable_geoip": disable_geoip,
}
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
resp_data = decide(self.api_key, self.host, timeout=self.feature_flags_request_timeout_seconds, **request_data)
return resp_data
@@ -374,7 +379,14 @@ class Client(object):
if self.sync_mode:
self.log.debug("enqueued with blocking %s.", msg["event"])
batch_post(self.api_key, self.host, gzip=self.gzip, timeout=self.timeout, batch=[msg])
batch_post(
self.api_key,
self.host,
gzip=self.gzip,
timeout=self.timeout,
batch=[msg],
historical_migration=self.historical_migration,
)
return True, msg
+10 -1
View File
@@ -36,6 +36,7 @@ class Consumer(Thread):
gzip=False,
retries=10,
timeout=15,
historical_migration=False,
):
"""Create a consumer thread."""
Thread.__init__(self)
@@ -55,6 +56,7 @@ class Consumer(Thread):
self.running = True
self.retries = retries
self.timeout = timeout
self.historical_migration = historical_migration
def run(self):
"""Runs the consumer."""
@@ -134,6 +136,13 @@ class Consumer(Thread):
@backoff.on_exception(backoff.expo, Exception, max_tries=self.retries + 1, giveup=fatal_exception)
def send_request():
batch_post(self.api_key, self.host, gzip=self.gzip, timeout=self.timeout, batch=batch)
batch_post(
self.api_key,
self.host,
gzip=self.gzip,
timeout=self.timeout,
batch=batch,
historical_migration=self.historical_migration,
)
send_request()
+5
View File
@@ -13,6 +13,8 @@ __LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)
log = logging.getLogger("posthog")
NONE_VALUES_ALLOWED_OPERATORS = ["is_not"]
class InconclusiveMatchError(Exception):
pass
@@ -119,6 +121,9 @@ def match_property(property, property_values) -> bool:
override_value = property_values[key]
if (operator not in NONE_VALUES_ALLOWED_OPERATORS) and override_value is None:
return False
if operator in ("exact", "is_not"):
def compute_exact_match(value, override_value):
+15 -1
View File
@@ -13,10 +13,24 @@ from posthog.version import VERSION
_session = requests.sessions.Session()
DEFAULT_HOST = "https://app.posthog.com"
US_INGESTION_ENDPOINT = "https://us.i.posthog.com"
EU_INGESTION_ENDPOINT = "https://eu.i.posthog.com"
DEFAULT_HOST = US_INGESTION_ENDPOINT
USER_AGENT = "posthog-python/" + VERSION
def determine_server_host(host: Optional[str]) -> str:
"""Determines the server host to use."""
host_or_default = host or DEFAULT_HOST
trimmed_host = remove_trailing_slash(host_or_default)
if trimmed_host in ("https://app.posthog.com", "https://us.posthog.com"):
return US_INGESTION_ENDPOINT
elif trimmed_host == "https://eu.posthog.com":
return EU_INGESTION_ENDPOINT
else:
return host_or_default
def post(
api_key: str, host: Optional[str] = None, path=None, gzip: bool = False, timeout: int = 15, **kwargs
) -> requests.Response:
+23 -18
View File
@@ -314,8 +314,8 @@ class TestClient(unittest.TestCase):
self.assertEqual(patch_decide.call_count, 1)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
"https://us.i.posthog.com",
timeout=3,
distinct_id="distinct_id",
groups={},
person_properties=None,
@@ -330,7 +330,12 @@ class TestClient(unittest.TestCase):
}
client = Client(
FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY, disable_geoip=True
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,
)
success, msg = client.capture("distinct_id", "python test event", send_feature_flags=True, disable_geoip=False)
client.flush()
@@ -351,8 +356,8 @@ class TestClient(unittest.TestCase):
self.assertEqual(patch_decide.call_count, 1)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
"https://us.i.posthog.com",
timeout=12,
distinct_id="distinct_id",
groups={},
person_properties=None,
@@ -779,8 +784,8 @@ class TestClient(unittest.TestCase):
client.get_feature_flag("random_key", "some_id", disable_geoip=True)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
"https://us.i.posthog.com",
timeout=3,
distinct_id="some_id",
groups={},
person_properties={"distinct_id": "some_id"},
@@ -791,8 +796,8 @@ class TestClient(unittest.TestCase):
client.feature_enabled("random_key", "feature_enabled_distinct_id", disable_geoip=True)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
"https://us.i.posthog.com",
timeout=3,
distinct_id="feature_enabled_distinct_id",
groups={},
person_properties={"distinct_id": "feature_enabled_distinct_id"},
@@ -803,8 +808,8 @@ class TestClient(unittest.TestCase):
client.get_all_flags_and_payloads("all_flags_payloads_id")
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
"https://us.i.posthog.com",
timeout=3,
distinct_id="all_flags_payloads_id",
groups={},
person_properties={"distinct_id": "all_flags_payloads_id"},
@@ -829,7 +834,7 @@ class TestClient(unittest.TestCase):
patch_decide.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 = 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",
@@ -839,8 +844,8 @@ class TestClient(unittest.TestCase):
)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
"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"},
@@ -865,8 +870,8 @@ class TestClient(unittest.TestCase):
)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
"http://app2.posthog.com",
timeout=3,
distinct_id="some_id",
groups={"company": "id:5", "instance": "app.posthog.com"},
person_properties={"distinct_id": "override"},
@@ -882,8 +887,8 @@ class TestClient(unittest.TestCase):
client.get_all_flags_and_payloads("some_id", groups={}, person_properties=None, group_properties=None)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
"http://app2.posthog.com",
timeout=3,
distinct_id="some_id",
groups={},
person_properties={"distinct_id": "some_id"},
+62 -10
View File
@@ -960,6 +960,60 @@ class TestLocalEvaluation(unittest.TestCase):
self.assertEqual(patch_decide.call_count, 0)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch("posthog.client.decide")
@mock.patch("posthog.client.get")
def test_feature_flags_local_evaluation_None_values(self, patch_get, patch_decide):
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
id: 1,
"name": "Beta Feature",
"key": "beta-feature",
"is_simple_flag": True,
"active": True,
"filters": {
"groups": [
{
"variant": None,
"properties": [
{"key": "latestBuildVersion", "type": "person", "value": ".+", "operator": "regex"},
{"key": "latestBuildVersionMajor", "type": "person", "value": "23", "operator": "gt"},
{"key": "latestBuildVersionMinor", "type": "person", "value": "31", "operator": "gt"},
{"key": "latestBuildVersionPatch", "type": "person", "value": "0", "operator": "gt"},
],
"rollout_percentage": 100,
}
],
},
},
]
feature_flag_match = client.get_feature_flag(
"beta-feature",
"some-distinct-id",
person_properties={
"latestBuildVersion": None,
"latestBuildVersionMajor": None,
"latestBuildVersionMinor": None,
"latestBuildVersionPatch": None,
},
)
self.assertEqual(feature_flag_match, False)
self.assertEqual(patch_decide.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
feature_flag_match = client.get_feature_flag(
"beta-feature",
"some-distinct-id",
person_properties={
"latestBuildVersion": "24.32..1",
"latestBuildVersionMajor": "24",
"latestBuildVersionMinor": "32",
"latestBuildVersionPatch": "1",
},
)
@mock.patch("posthog.client.decide")
@mock.patch("posthog.client.get")
def test_feature_flags_local_evaluation_for_cohorts(self, patch_get, patch_decide):
@@ -1714,7 +1768,7 @@ class TestMatchProperties(unittest.TestCase):
self.assertTrue(match_property(property_a, {"key": "value"}))
self.assertTrue(match_property(property_a, {"key": "value2"}))
self.assertTrue(match_property(property_a, {"key": ""}))
self.assertTrue(match_property(property_a, {"key": None}))
self.assertFalse(match_property(property_a, {"key": None}))
with self.assertRaises(InconclusiveMatchError):
match_property(property_a, {"key2": "value"})
@@ -1980,20 +2034,20 @@ class TestMatchProperties(unittest.TestCase):
self.assertTrue(match_property(property_a, {"key": "non"}))
property_b = self.property(key="key", value=None, operator="is_set")
self.assertTrue(match_property(property_b, {"key": None}))
self.assertFalse(match_property(property_b, {"key": None}))
property_c = self.property(key="key", value="no", operator="icontains")
self.assertTrue(match_property(property_c, {"key": None}))
self.assertFalse(match_property(property_c, {"key": None}))
self.assertFalse(match_property(property_c, {"key": "smh"}))
property_d = self.property(key="key", value="No", operator="regex")
self.assertTrue(match_property(property_d, {"key": None}))
self.assertFalse(match_property(property_d, {"key": None}))
property_d_lower_case = self.property(key="key", value="no", operator="regex")
self.assertFalse(match_property(property_d_lower_case, {"key": None}))
property_e = self.property(key="key", value=1, operator="gt")
self.assertTrue(match_property(property_e, {"key": None}))
self.assertFalse(match_property(property_e, {"key": None}))
property_f = self.property(key="key", value=1, operator="lt")
self.assertFalse(match_property(property_f, {"key": None}))
@@ -2002,15 +2056,13 @@ class TestMatchProperties(unittest.TestCase):
self.assertFalse(match_property(property_g, {"key": None}))
property_h = self.property(key="key", value="Oo", operator="lte")
self.assertTrue(match_property(property_h, {"key": None}))
self.assertFalse(match_property(property_h, {"key": None}))
property_i = self.property(key="key", value="2022-05-01", operator="is_date_before")
with self.assertRaises(InconclusiveMatchError):
self.assertFalse(match_property(property_i, {"key": None}))
self.assertFalse(match_property(property_i, {"key": None}))
property_j = self.property(key="key", value="2022-05-01", operator="is_date_after")
with self.assertRaises(InconclusiveMatchError):
self.assertFalse(match_property(property_j, {"key": None}))
self.assertFalse(match_property(property_j, {"key": None}))
property_k = self.property(key="key", value="2022-05-01", operator="is_date_before")
with self.assertRaises(InconclusiveMatchError):
+25 -1
View File
@@ -2,9 +2,10 @@ import json
import unittest
from datetime import date, datetime
import pytest
import requests
from posthog.request import DatetimeSerializer, batch_post
from posthog.request import DatetimeSerializer, batch_post, determine_server_host
from posthog.test.test_utils import TEST_API_KEY
@@ -42,3 +43,26 @@ class TestRequests(unittest.TestCase):
batch_post(
"key", batch=[{"distinct_id": "distinct_id", "event": "python event", "type": "track"}], timeout=0.0001
)
@pytest.mark.parametrize(
"host, expected",
[
("https://t.posthog.com", "https://t.posthog.com"),
("https://t.posthog.com/", "https://t.posthog.com/"),
("t.posthog.com", "t.posthog.com"),
("t.posthog.com/", "t.posthog.com/"),
("https://us.posthog.com.rg.proxy.com", "https://us.posthog.com.rg.proxy.com"),
("app.posthog.com", "app.posthog.com"),
("eu.posthog.com", "eu.posthog.com"),
("https://app.posthog.com", "https://us.i.posthog.com"),
("https://eu.posthog.com", "https://eu.i.posthog.com"),
("https://us.posthog.com", "https://us.i.posthog.com"),
("https://app.posthog.com/", "https://us.i.posthog.com"),
("https://eu.posthog.com/", "https://eu.i.posthog.com"),
("https://us.posthog.com/", "https://us.i.posthog.com"),
(None, "https://us.i.posthog.com"),
],
)
def test_routing_to_custom_host(host, expected):
assert determine_server_host(host) == expected
+1 -1
View File
@@ -1,4 +1,4 @@
VERSION = "3.3.3"
VERSION = "3.5.2"
if __name__ == "__main__":
print(VERSION, end="") # noqa: T201