Skip to content

Commit dac06ba

Browse files
authored
fix(feature-flags): Add more options to make using library easier (#70)
1 parent 2dc1298 commit dac06ba

File tree

7 files changed

+250
-37
lines changed

7 files changed

+250
-37
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 2.0.1 - 2022-08-04
2+
3+
- Make poll_interval configurable
4+
- Add `send_feature_flag_events` parameter to feature flag calls, which determine whether the `$feature_flag_called` event should be sent or not.
5+
- Add `only_evaluate_locally` parameter to feature flag calls, which determines whether the feature flag should only be evaluated locally or not.
6+
17
## 2.0.0 - 2022-08-02
28

39
Breaking changes:

example.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import posthog
77

8-
# posthog.debug = True
8+
posthog.debug = True
99

1010
# You can find this key on the /setup page in PostHog
1111
posthog.project_api_key = ""
@@ -14,15 +14,14 @@
1414
# Where you host PostHog, with no trailing /.
1515
# You can remove this line if you're using posthog.com
1616
posthog.host = "http://localhost:8000"
17+
posthog.poll_interval = 10
18+
1719

1820
# Capture an event
1921
posthog.capture("distinct_id", "event", {"property1": "value", "property2": "value"}, send_feature_flags=True)
2022

2123
print(posthog.feature_enabled("beta-feature", "distinct_id"))
22-
print(posthog.feature_enabled("beta-feature", "distinct_id", groups={"company": "id:5"}))
23-
24-
print("sleeping")
25-
# time.sleep(5)
24+
print(posthog.feature_enabled("beta-feature-groups", "distinct_id", groups={"company": "id:5"}))
2625

2726
print(posthog.feature_enabled("beta-feature", "distinct_id"))
2827

@@ -44,7 +43,6 @@
4443
# properties set only once to the person
4544
posthog.set_once("new_distinct_id", {"self_serve_signup": True})
4645

47-
# time.sleep(3)
4846

4947
posthog.set_once(
5048
"new_distinct_id", {"self_serve_signup": False}
@@ -53,7 +51,6 @@
5351
posthog.set("new_distinct_id", {"current_browser": "Chrome"})
5452
posthog.set("new_distinct_id", {"current_browser": "Firefox"})
5553

56-
# posthog.shutdown()
5754

5855
# #############################################################################
5956
# Make sure you have a personal API key for the examples below
@@ -63,4 +60,23 @@
6360
# If flag has City=Sydney, this call doesn't go to `/decide`
6461
print(posthog.feature_enabled("test-flag", "distinct_id_random_22", person_properties={"$geoip_city_name": "Sydney"}))
6562

63+
print(
64+
posthog.feature_enabled(
65+
"test-flag",
66+
"distinct_id_random_22",
67+
person_properties={"$geoip_city_name": "Sydney"},
68+
only_evaluate_locally=True,
69+
)
70+
)
71+
72+
6673
print(posthog.get_all_flags("distinct_id_random_22"))
74+
print(posthog.get_all_flags("distinct_id_random_22", only_evaluate_locally=True))
75+
print(
76+
posthog.get_all_flags(
77+
"distinct_id_random_22", person_properties={"$geoip_city_name": "Sydney"}, only_evaluate_locally=True
78+
)
79+
)
80+
81+
82+
posthog.shutdown()

posthog/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
disabled = False # type: bool
1616
personal_api_key = None # type: str
1717
project_api_key = None # type: str
18+
poll_interval = 30 # type: int
1819

1920
default_client = None
2021

@@ -238,6 +239,8 @@ def feature_enabled(
238239
groups={}, # type: dict
239240
person_properties={}, # type: dict
240241
group_properties={}, # type: dict
242+
only_evaluate_locally=False, # type: bool
243+
send_feature_flag_events=True, # type: bool
241244
):
242245
# type: (...) -> bool
243246
"""
@@ -261,6 +264,8 @@ def feature_enabled(
261264
groups=groups,
262265
person_properties=person_properties,
263266
group_properties=group_properties,
267+
only_evaluate_locally=only_evaluate_locally,
268+
send_feature_flag_events=send_feature_flag_events,
264269
)
265270

266271

@@ -271,6 +276,8 @@ def get_feature_flag(
271276
groups={}, # type: dict
272277
person_properties={}, # type: dict
273278
group_properties={}, # type: dict
279+
only_evaluate_locally=False, # type: bool
280+
send_feature_flag_events=True, # type: bool
274281
):
275282
"""
276283
Get feature flag variant for users. Used with experiments.
@@ -302,6 +309,8 @@ def get_feature_flag(
302309
groups=groups,
303310
person_properties=person_properties,
304311
group_properties=group_properties,
312+
only_evaluate_locally=only_evaluate_locally,
313+
send_feature_flag_events=send_feature_flag_events,
305314
)
306315

307316

@@ -310,6 +319,7 @@ def get_all_flags(
310319
groups={}, # type: dict
311320
person_properties={}, # type: dict
312321
group_properties={}, # type: dict
322+
only_evaluate_locally=False, # type: bool
313323
):
314324
"""
315325
Get all flags for a given user.
@@ -326,6 +336,7 @@ def get_all_flags(
326336
groups=groups,
327337
person_properties=person_properties,
328338
group_properties=group_properties,
339+
only_evaluate_locally=only_evaluate_locally,
329340
)
330341

331342

@@ -370,6 +381,7 @@ def _proxy(method, *args, **kwargs):
370381
sync_mode=sync_mode,
371382
personal_api_key=personal_api_key,
372383
project_api_key=project_api_key,
384+
poll_interval=poll_interval,
373385
)
374386

375387
fn = getattr(default_client, method)

posthog/client.py

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import atexit
22
import logging
33
import numbers
4-
from collections import defaultdict
54
from datetime import datetime, timedelta
6-
from tokenize import group
7-
from uuid import UUID, uuid4
5+
from uuid import UUID
86

97
from dateutil.tz import tzutc
108
from six import string_types
@@ -25,6 +23,10 @@
2523
ID_TYPES = (numbers.Number, string_types, UUID)
2624
MAX_DICT_SIZE = 50_000
2725

26+
# Ensures that debug level messages are logged when debug mode is on.
27+
# Otherwise, defaults to WARNING level. See https://docs.python.org/3/howto/logging.html#what-happens-if-no-configuration-is-provided
28+
logging.basicConfig()
29+
2830

2931
class Client(object):
3032
"""Create a new PostHog client."""
@@ -73,13 +75,10 @@ def __init__(
7375

7476
# personal_api_key: This should be a generated Personal API Key, private
7577
self.personal_api_key = personal_api_key
76-
7778
if debug:
78-
# Ensures that debug level messages are logged when debug mode is on.
79-
# Otherwise, defaults to WARNING level. See https://docs.python.org/3/howto/logging.html#what-happens-if-no-configuration-is-provided
80-
logging.basicConfig(level=logging.DEBUG)
79+
self.log.setLevel(logging.DEBUG)
8180
else:
82-
logging.basicConfig(level=logging.WARNING)
81+
self.log.setLevel(logging.WARNING)
8382

8483
if sync_mode:
8584
self.consumers = None
@@ -375,7 +374,7 @@ def _load_feature_flags(self):
375374
"More information: https://posthog.com/docs/api/overview",
376375
)
377376
else:
378-
raise APIError(status=e.status, message=e.message)
377+
self.log.error(f"[FEATURE FLAGS] Error loading feature flags: {e}")
379378
except Exception as e:
380379
self.log.warning(
381380
"[FEATURE FLAGS] Fetching feature flags failed with following error. We will retry in %s seconds."
@@ -429,7 +428,18 @@ def _compute_flag_locally(self, feature_flag, distinct_id, *, groups={}, person_
429428
else:
430429
return match_feature_flag_properties(feature_flag, distinct_id, person_properties)
431430

432-
def feature_enabled(self, key, distinct_id, default=False, *, groups={}, person_properties={}, group_properties={}):
431+
def feature_enabled(
432+
self,
433+
key,
434+
distinct_id,
435+
default=False,
436+
*,
437+
groups={},
438+
person_properties={},
439+
group_properties={},
440+
only_evaluate_locally=False,
441+
send_feature_flag_events=True,
442+
):
433443
return bool(
434444
self.get_feature_flag(
435445
key,
@@ -438,11 +448,22 @@ def feature_enabled(self, key, distinct_id, default=False, *, groups={}, person_
438448
groups=groups,
439449
person_properties=person_properties,
440450
group_properties=group_properties,
451+
only_evaluate_locally=only_evaluate_locally,
452+
send_feature_flag_events=send_feature_flag_events,
441453
)
442454
)
443455

444456
def get_feature_flag(
445-
self, key, distinct_id, default=False, *, groups={}, person_properties={}, group_properties={}
457+
self,
458+
key,
459+
distinct_id,
460+
default=False,
461+
*,
462+
groups={},
463+
person_properties={},
464+
group_properties={},
465+
only_evaluate_locally=False,
466+
send_feature_flag_events=True,
446467
):
447468
require("key", key, string_types)
448469
require("distinct_id", distinct_id, ID_TYPES)
@@ -472,7 +493,8 @@ def get_feature_flag(
472493
self.log.exception(f"[FEATURE FLAGS] Error while computing variant locally: {e}")
473494
continue
474495

475-
if response is None:
496+
flag_was_locally_evaluated = response is not None
497+
if not flag_was_locally_evaluated and not only_evaluate_locally:
476498
try:
477499
feature_flags = self.get_feature_variants(
478500
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
@@ -483,14 +505,27 @@ def get_feature_flag(
483505
self.log.exception(f"[FEATURE FLAGS] Unable to get flag remotely: {e}")
484506
response = default
485507

486-
if key not in self.distinct_ids_feature_flags_reported[distinct_id]:
508+
feature_flag_reported_key = f"{key}_{str(response)}"
509+
if (
510+
feature_flag_reported_key not in self.distinct_ids_feature_flags_reported[distinct_id]
511+
and send_feature_flag_events
512+
):
487513
self.capture(
488-
distinct_id, "$feature_flag_called", {"$feature_flag": key, "$feature_flag_response": response}
514+
distinct_id,
515+
"$feature_flag_called",
516+
{
517+
"$feature_flag": key,
518+
"$feature_flag_response": response,
519+
"locally_evaluated": flag_was_locally_evaluated,
520+
},
521+
groups=groups,
489522
)
490-
self.distinct_ids_feature_flags_reported[distinct_id].add(key)
523+
self.distinct_ids_feature_flags_reported[distinct_id].add(feature_flag_reported_key)
491524
return response
492525

493-
def get_all_flags(self, distinct_id, *, groups={}, person_properties={}, group_properties={}):
526+
def get_all_flags(
527+
self, distinct_id, *, groups={}, person_properties={}, group_properties={}, only_evaluate_locally=False
528+
):
494529
require("distinct_id", distinct_id, ID_TYPES)
495530
require("groups", groups, dict)
496531

@@ -520,7 +555,7 @@ def get_all_flags(self, distinct_id, *, groups={}, person_properties={}, group_p
520555
else:
521556
fallback_to_decide = True
522557

523-
if fallback_to_decide:
558+
if fallback_to_decide and not only_evaluate_locally:
524559
try:
525560
feature_flags = self.get_feature_variants(
526561
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties

posthog/request.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,14 @@ def _process_response(
5050
res: requests.Response, success_message: str, *, return_json: bool = True
5151
) -> Union[requests.Response, Any]:
5252
log = logging.getLogger("posthog")
53-
if not res:
54-
raise APIError(
55-
"N/A",
56-
"Error when fetching PostHog API, please make sure you are using your public project token/key and not a private API key.",
57-
)
5853
if res.status_code == 200:
5954
log.debug(success_message)
6055
return res.json() if return_json else res
6156
try:
6257
payload = res.json()
6358
log.debug("received response: %s", payload)
6459
raise APIError(res.status_code, payload["detail"])
65-
except ValueError:
60+
except (KeyError, ValueError):
6661
raise APIError(res.status_code, res.text)
6762

6863

0 commit comments

Comments
 (0)