Compare commits

...
3 Commits
Author SHA1 Message Date
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
Neil KakkarandGitHub 8554b51a48 fix(flags): Update relative date op names (#113) 2024-01-26 15:52:46 +00:00
11 changed files with 138 additions and 61 deletions
+12
View File
@@ -1,3 +1,15 @@
## 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
## 3.3.2 - 2024-01-19
1. Return success/failure with all capture calls from module functions
+11 -6
View File
@@ -7,19 +7,19 @@ 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
default_client = None
default_client = None # type: Optional[Client]
def capture(
@@ -405,6 +405,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)
+25 -8
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
@@ -61,7 +61,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
@@ -464,7 +464,16 @@ class Client(object):
self.poller = Poller(interval=timedelta(seconds=self.poll_interval), execute=self._load_feature_flags)
self.poller.start()
def _compute_flag_locally(self, feature_flag, distinct_id, *, groups={}, person_properties={}, group_properties={}):
def _compute_flag_locally(
self,
feature_flag,
distinct_id,
*,
groups={},
person_properties={},
group_properties={},
warn_on_unknown_groups=True,
):
if feature_flag.get("ensure_experience_continuity", False):
raise InconclusiveMatchError("Flag has experience continuity enabled")
@@ -486,9 +495,14 @@ class Client(object):
if group_name not in groups:
# Group flags are never enabled in `groups` aren't passed in
# don't failover to `/decide/`, since response will be the same
self.log.warning(
f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
)
if warn_on_unknown_groups:
self.log.warning(
f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
)
else:
self.log.debug(
f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
)
return False
focused_group_properties = group_properties[group_name]
@@ -717,7 +731,9 @@ class Client(object):
return response
def _get_all_flags_and_payloads_locally(self, distinct_id, *, groups={}, person_properties={}, group_properties={}):
def _get_all_flags_and_payloads_locally(
self, distinct_id, *, groups={}, person_properties={}, group_properties={}, warn_on_unknown_groups=False
):
require("distinct_id", distinct_id, ID_TYPES)
require("groups", groups, dict)
@@ -737,6 +753,7 @@ class Client(object):
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
warn_on_unknown_groups=warn_on_unknown_groups,
)
matched_payload = self._compute_payload_locally(flag["key"], flags[flag["key"]])
if matched_payload:
@@ -756,7 +773,7 @@ class Client(object):
return self.feature_flags
def _add_local_person_and_group_properties(self, distinct_id, groups, person_properties, group_properties):
all_person_properties = {"$current_distinct_id": distinct_id, **(person_properties or {})}
all_person_properties = {"distinct_id": distinct_id, **(person_properties or {})}
all_group_properties = {}
if groups:
+8 -8
View File
@@ -175,11 +175,11 @@ def match_property(property, property_values) -> bool:
else:
return compare(str(override_value), str(value), operator)
if operator in ["is_date_before", "is_date_after", "is_relative_date_before", "is_relative_date_after"]:
if operator in ["is_date_before", "is_date_after"]:
try:
if operator in ["is_relative_date_before", "is_relative_date_after"]:
parsed_date = relative_date_parse_for_feature_flag_matching(str(value))
else:
parsed_date = relative_date_parse_for_feature_flag_matching(str(value))
if not parsed_date:
parsed_date = parser.parse(str(value))
parsed_date = convert_to_datetime_aware(parsed_date)
except Exception as e:
@@ -190,12 +190,12 @@ def match_property(property, property_values) -> bool:
if isinstance(override_value, datetime.datetime):
override_date = convert_to_datetime_aware(override_value)
if operator in ("is_date_before", "is_relative_date_before"):
if operator == "is_date_before":
return override_date < parsed_date
else:
return override_date > parsed_date
elif isinstance(override_value, datetime.date):
if operator in ("is_date_before", "is_relative_date_before"):
if operator == "is_date_before":
return override_value < parsed_date.date()
else:
return override_value > parsed_date.date()
@@ -203,7 +203,7 @@ def match_property(property, property_values) -> bool:
try:
override_date = parser.parse(override_value)
override_date = convert_to_datetime_aware(override_date)
if operator in ("is_date_before", "is_relative_date_before"):
if operator == "is_date_before":
return override_date < parsed_date
else:
return override_date > parsed_date
@@ -302,7 +302,7 @@ def match_property_group(property_group, property_values, cohort_properties) ->
def relative_date_parse_for_feature_flag_matching(value: str) -> Optional[datetime.datetime]:
regex = r"^(?P<number>[0-9]+)(?P<interval>[a-z])$"
regex = r"^-?(?P<number>[0-9]+)(?P<interval>[a-z])$"
match = re.search(regex, value)
parsed_dt = datetime.datetime.now(datetime.timezone.utc)
if match:
+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-api.i.posthog.com"
EU_INGESTION_ENDPOINT = "https://eu-api.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:
+3 -3
View File
@@ -43,9 +43,9 @@ class PostHogIntegration(Integration):
not not Hub.current.client.dsn and Dsn(Hub.current.client.dsn).project_id
)
if project_id:
properties[
"$sentry_url"
] = f"{PostHogIntegration.prefix}{PostHogIntegration.organization}/issues/?project={project_id}&query={event['event_id']}"
properties["$sentry_url"] = (
f"{PostHogIntegration.prefix}{PostHogIntegration.organization}/issues/?project={project_id}&query={event['event_id']}"
)
posthog.capture(posthog_distinct_id, "$exception", properties)
+21 -17
View File
@@ -314,7 +314,7 @@ class TestClient(unittest.TestCase):
self.assertEqual(patch_decide.call_count, 1)
patch_decide.assert_called_with(
"random_key",
None,
"https://us-api.i.posthog.com",
timeout=10,
distinct_id="distinct_id",
groups={},
@@ -330,7 +330,11 @@ 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,
)
success, msg = client.capture("distinct_id", "python test event", send_feature_flags=True, disable_geoip=False)
client.flush()
@@ -351,7 +355,7 @@ class TestClient(unittest.TestCase):
self.assertEqual(patch_decide.call_count, 1)
patch_decide.assert_called_with(
"random_key",
None,
"https://us-api.i.posthog.com",
timeout=10,
distinct_id="distinct_id",
groups={},
@@ -779,11 +783,11 @@ class TestClient(unittest.TestCase):
client.get_feature_flag("random_key", "some_id", disable_geoip=True)
patch_decide.assert_called_with(
"random_key",
None,
"https://us-api.i.posthog.com",
timeout=10,
distinct_id="some_id",
groups={},
person_properties={"$current_distinct_id": "some_id"},
person_properties={"distinct_id": "some_id"},
group_properties={},
disable_geoip=True,
)
@@ -791,11 +795,11 @@ class TestClient(unittest.TestCase):
client.feature_enabled("random_key", "feature_enabled_distinct_id", disable_geoip=True)
patch_decide.assert_called_with(
"random_key",
None,
"https://us-api.i.posthog.com",
timeout=10,
distinct_id="feature_enabled_distinct_id",
groups={},
person_properties={"$current_distinct_id": "feature_enabled_distinct_id"},
person_properties={"distinct_id": "feature_enabled_distinct_id"},
group_properties={},
disable_geoip=True,
)
@@ -803,11 +807,11 @@ class TestClient(unittest.TestCase):
client.get_all_flags_and_payloads("all_flags_payloads_id")
patch_decide.assert_called_with(
"random_key",
None,
"https://us-api.i.posthog.com",
timeout=10,
distinct_id="all_flags_payloads_id",
groups={},
person_properties={"$current_distinct_id": "all_flags_payloads_id"},
person_properties={"distinct_id": "all_flags_payloads_id"},
group_properties={},
disable_geoip=False,
)
@@ -829,7 +833,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,11 +843,11 @@ class TestClient(unittest.TestCase):
)
patch_decide.assert_called_with(
"random_key",
None,
"http://app2.posthog.com",
timeout=10,
distinct_id="some_id",
groups={"company": "id:5", "instance": "app.posthog.com"},
person_properties={"$current_distinct_id": "some_id", "x1": "y1"},
person_properties={"distinct_id": "some_id", "x1": "y1"},
group_properties={
"company": {"$group_key": "id:5", "x": "y"},
"instance": {"$group_key": "app.posthog.com"},
@@ -856,7 +860,7 @@ class TestClient(unittest.TestCase):
"random_key",
"some_id",
groups={"company": "id:5", "instance": "app.posthog.com"},
person_properties={"$current_distinct_id": "override"},
person_properties={"distinct_id": "override"},
group_properties={
"company": {
"$group_key": "group_override",
@@ -865,11 +869,11 @@ class TestClient(unittest.TestCase):
)
patch_decide.assert_called_with(
"random_key",
None,
"http://app2.posthog.com",
timeout=10,
distinct_id="some_id",
groups={"company": "id:5", "instance": "app.posthog.com"},
person_properties={"$current_distinct_id": "override"},
person_properties={"distinct_id": "override"},
group_properties={
"company": {"$group_key": "group_override"},
"instance": {"$group_key": "app.posthog.com"},
@@ -882,11 +886,11 @@ 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,
"http://app2.posthog.com",
timeout=10,
distinct_id="some_id",
groups={},
person_properties={"$current_distinct_id": "some_id"},
person_properties={"distinct_id": "some_id"},
group_properties={},
disable_geoip=False,
)
+16 -16
View File
@@ -1873,7 +1873,7 @@ class TestMatchProperties(unittest.TestCase):
@freeze_time("2022-05-01")
def test_match_property_relative_date_operators(self):
property_a = self.property(key="key", value="6h", operator="is_relative_date_before")
property_a = self.property(key="key", value="-6h", operator="is_date_before")
self.assertTrue(match_property(property_a, {"key": "2022-03-01"}))
self.assertTrue(match_property(property_a, {"key": "2022-04-30"}))
self.assertTrue(match_property(property_a, {"key": datetime.datetime(2022, 4, 30, 1, 2, 3)}))
@@ -1898,7 +1898,7 @@ class TestMatchProperties(unittest.TestCase):
with self.assertRaises(InconclusiveMatchError):
match_property(property_a, {"key": "abcdef"})
property_b = self.property(key="key", value="1h", operator="is_relative_date_after")
property_b = self.property(key="key", value="1h", operator="is_date_after")
self.assertTrue(match_property(property_b, {"key": "2022-05-02"}))
self.assertTrue(match_property(property_b, {"key": "2022-05-30"}))
self.assertTrue(match_property(property_b, {"key": datetime.datetime(2022, 5, 30)}))
@@ -1910,16 +1910,16 @@ class TestMatchProperties(unittest.TestCase):
self.assertFalse(match_property(property_b, {"key": "abcdef"}))
# Invalid flag property
property_c = self.property(key="key", value=1234, operator="is_relative_date_after")
property_c = self.property(key="key", value=1234, operator="is_date_after")
with self.assertRaises(InconclusiveMatchError):
self.assertFalse(match_property(property_c, {"key": 1}))
with self.assertRaises(InconclusiveMatchError):
self.assertFalse(match_property(property_c, {"key": "2022-05-30"}))
# parsed as 1234-05-01 for some reason?
self.assertTrue(match_property(property_c, {"key": "2022-05-30"}))
# # Timezone aware property
property_d = self.property(key="key", value="12d", operator="is_relative_date_before")
property_d = self.property(key="key", value="12d", operator="is_date_before")
self.assertFalse(match_property(property_d, {"key": "2022-05-30"}))
self.assertTrue(match_property(property_d, {"key": "2022-03-30"}))
@@ -1929,45 +1929,45 @@ class TestMatchProperties(unittest.TestCase):
self.assertFalse(match_property(property_d, {"key": "2022-04-19 02:00:01+02:00"}))
# Try all possible relative dates
property_e = self.property(key="key", value="1h", operator="is_relative_date_before")
property_e = self.property(key="key", value="1h", operator="is_date_before")
self.assertFalse(match_property(property_e, {"key": "2022-05-01 00:00:00"}))
self.assertTrue(match_property(property_e, {"key": "2022-04-30 22:00:00"}))
property_f = self.property(key="key", value="1d", operator="is_relative_date_before")
property_f = self.property(key="key", value="-1d", operator="is_date_before")
self.assertTrue(match_property(property_f, {"key": "2022-04-29 23:59:00"}))
self.assertFalse(match_property(property_f, {"key": "2022-04-30 00:00:01"}))
property_g = self.property(key="key", value="1w", operator="is_relative_date_before")
property_g = self.property(key="key", value="1w", operator="is_date_before")
self.assertTrue(match_property(property_g, {"key": "2022-04-23 00:00:00"}))
self.assertFalse(match_property(property_g, {"key": "2022-04-24 00:00:00"}))
self.assertFalse(match_property(property_g, {"key": "2022-04-24 00:00:01"}))
property_h = self.property(key="key", value="1m", operator="is_relative_date_before")
property_h = self.property(key="key", value="1m", operator="is_date_before")
self.assertTrue(match_property(property_h, {"key": "2022-03-01 00:00:00"}))
self.assertFalse(match_property(property_h, {"key": "2022-04-05 00:00:00"}))
property_i = self.property(key="key", value="1y", operator="is_relative_date_before")
property_i = self.property(key="key", value="1y", operator="is_date_before")
self.assertTrue(match_property(property_i, {"key": "2021-04-28 00:00:00"}))
self.assertFalse(match_property(property_i, {"key": "2021-05-01 00:00:01"}))
property_j = self.property(key="key", value="122h", operator="is_relative_date_after")
property_j = self.property(key="key", value="122h", operator="is_date_after")
self.assertTrue(match_property(property_j, {"key": "2022-05-01 00:00:00"}))
self.assertFalse(match_property(property_j, {"key": "2022-04-23 01:00:00"}))
property_k = self.property(key="key", value="2d", operator="is_relative_date_after")
property_k = self.property(key="key", value="2d", operator="is_date_after")
self.assertTrue(match_property(property_k, {"key": "2022-05-01 00:00:00"}))
self.assertTrue(match_property(property_k, {"key": "2022-04-29 00:00:01"}))
self.assertFalse(match_property(property_k, {"key": "2022-04-29 00:00:00"}))
property_l = self.property(key="key", value="02w", operator="is_relative_date_after")
property_l = self.property(key="key", value="-02w", operator="is_date_after")
self.assertTrue(match_property(property_l, {"key": "2022-05-01 00:00:00"}))
self.assertFalse(match_property(property_l, {"key": "2022-04-16 00:00:00"}))
property_m = self.property(key="key", value="1m", operator="is_relative_date_after")
property_m = self.property(key="key", value="1m", operator="is_date_after")
self.assertTrue(match_property(property_m, {"key": "2022-04-01 00:00:01"}))
self.assertFalse(match_property(property_m, {"key": "2022-04-01 00:00:00"}))
property_n = self.property(key="key", value="1y", operator="is_relative_date_after")
property_n = self.property(key="key", value="1y", operator="is_date_after")
self.assertTrue(match_property(property_n, {"key": "2022-05-01 00:00:00"}))
self.assertTrue(match_property(property_n, {"key": "2021-05-01 00:00:01"}))
self.assertFalse(match_property(property_n, {"key": "2021-05-01 00:00:00"}))
+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-api.i.posthog.com"),
("https://eu.posthog.com", "https://eu-api.i.posthog.com"),
("https://us.posthog.com", "https://us-api.i.posthog.com"),
("https://app.posthog.com/", "https://us-api.i.posthog.com"),
("https://eu.posthog.com/", "https://eu-api.i.posthog.com"),
("https://us.posthog.com/", "https://us-api.i.posthog.com"),
(None, "https://us-api.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.2"
VERSION = "3.4.0"
if __name__ == "__main__":
print(VERSION, end="") # noqa: T201
@@ -13,6 +13,7 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path