Skip to content

Commit d7be253

Browse files
authored
feat(feature-flags): JSON payload function (#81)
* implementation with test * format * v=3 and get_all_payloads * format * fix bug * format * address comments * format * add tests * format * add resiliency * fix lookup * remove check * address comments * example.py * format * format
1 parent 592c0f3 commit d7be253

File tree

5 files changed

+490
-19
lines changed

5 files changed

+490
-19
lines changed

example.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
)
3232
)
3333

34-
exit()
3534

3635
# Capture an event
3736
posthog.capture("distinct_id", "event", {"property1": "value", "property2": "value"}, send_feature_flags=True)
@@ -41,6 +40,10 @@
4140

4241
print(posthog.feature_enabled("beta-feature", "distinct_id"))
4342

43+
# get payload
44+
print(posthog.get_feature_flag_payload("beta-feature", "distinct_id"))
45+
print(posthog.get_all_flags_and_payloads("distinct_id"))
46+
exit()
4447
# # Alias a previous distinct id with a new one
4548

4649
posthog.alias("distinct_id", "new_distinct_id")

posthog/__init__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,46 @@ def get_all_flags(
336336
)
337337

338338

339+
def get_feature_flag_payload(
340+
key,
341+
distinct_id,
342+
match_value=None,
343+
groups={},
344+
person_properties={},
345+
group_properties={},
346+
only_evaluate_locally=False,
347+
send_feature_flag_events=True,
348+
):
349+
return _proxy(
350+
"get_feature_flag_payload",
351+
key=key,
352+
distinct_id=distinct_id,
353+
match_value=match_value,
354+
groups=groups,
355+
person_properties=person_properties,
356+
group_properties=group_properties,
357+
only_evaluate_locally=only_evaluate_locally,
358+
send_feature_flag_events=send_feature_flag_events,
359+
)
360+
361+
362+
def get_all_flags_and_payloads(
363+
distinct_id,
364+
groups={},
365+
person_properties={},
366+
group_properties={},
367+
only_evaluate_locally=False,
368+
):
369+
return _proxy(
370+
"get_all_flags_and_payloads",
371+
distinct_id=distinct_id,
372+
groups=groups,
373+
person_properties=person_properties,
374+
group_properties=group_properties,
375+
only_evaluate_locally=only_evaluate_locally,
376+
)
377+
378+
339379
def page(*args, **kwargs):
340380
"""Send a page call."""
341381
_proxy("page", *args, **kwargs)

posthog/client.py

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def __init__(
6464
self.gzip = gzip
6565
self.timeout = timeout
6666
self.feature_flags = None
67+
self.feature_flags_by_key = None
6768
self.group_type_mapping = None
6869
self.poll_interval = poll_interval
6970
self.poller = None
@@ -127,6 +128,14 @@ def identify(self, distinct_id=None, properties=None, context=None, timestamp=No
127128
return self._enqueue(msg)
128129

129130
def get_feature_variants(self, distinct_id, groups=None, person_properties=None, group_properties=None):
131+
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties)
132+
return resp_data["featureFlags"]
133+
134+
def get_feature_payloads(self, distinct_id, groups=None, person_properties=None, group_properties=None):
135+
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties)
136+
return resp_data["featureFlagPayloads"]
137+
138+
def get_decide(self, distinct_id, groups=None, person_properties=None, group_properties=None):
130139
require("distinct_id", distinct_id, ID_TYPES)
131140

132141
if groups:
@@ -141,7 +150,7 @@ def get_feature_variants(self, distinct_id, groups=None, person_properties=None,
141150
"group_properties": group_properties,
142151
}
143152
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
144-
return resp_data["featureFlags"]
153+
return resp_data
145154

146155
def capture(
147156
self,
@@ -358,10 +367,18 @@ def shutdown(self):
358367

359368
def _load_feature_flags(self):
360369
try:
370+
361371
response = get(
362-
self.personal_api_key, f"/api/feature_flag/local_evaluation/?token={self.api_key}", self.host
372+
self.personal_api_key,
373+
f"/api/feature_flag/local_evaluation/?token={self.api_key}",
374+
self.host,
375+
timeout=10,
363376
)
377+
364378
self.feature_flags = response["flags"] or []
379+
self.feature_flags_by_key = {
380+
flag["key"]: flag for flag in self.feature_flags if flag.get("key") is not None
381+
}
365382
self.group_type_mapping = response["group_type_mapping"] or {}
366383

367384
except APIError as e:
@@ -522,48 +539,117 @@ def get_feature_flag(
522539
self.distinct_ids_feature_flags_reported[distinct_id].add(feature_flag_reported_key)
523540
return response
524541

542+
def get_feature_flag_payload(
543+
self,
544+
key,
545+
distinct_id,
546+
*,
547+
match_value=None,
548+
groups={},
549+
person_properties={},
550+
group_properties={},
551+
only_evaluate_locally=False,
552+
send_feature_flag_events=True,
553+
):
554+
if match_value is None:
555+
match_value = self.get_feature_flag(
556+
key,
557+
distinct_id,
558+
groups=groups,
559+
person_properties=person_properties,
560+
group_properties=group_properties,
561+
send_feature_flag_events=send_feature_flag_events,
562+
only_evaluate_locally=True,
563+
)
564+
565+
response = None
566+
567+
if match_value is not None:
568+
response = self._compute_payload_locally(key, match_value)
569+
570+
if response is None and not only_evaluate_locally:
571+
decide_payloads = self.get_feature_payloads(distinct_id, groups, person_properties, group_properties)
572+
response = decide_payloads.get(str(key).lower(), None)
573+
574+
return response
575+
576+
def _compute_payload_locally(self, key, match_value):
577+
payload = None
578+
579+
if self.feature_flags_by_key is None:
580+
return payload
581+
582+
flag_definition = self.feature_flags_by_key.get(key) or {}
583+
flag_filters = flag_definition.get("filters") or {}
584+
flag_payloads = flag_filters.get("payloads") or {}
585+
payload = flag_payloads.get(str(match_value).lower(), None)
586+
return payload
587+
525588
def get_all_flags(
526589
self, distinct_id, *, groups={}, person_properties={}, group_properties={}, only_evaluate_locally=False
527590
):
591+
flags = self.get_all_flags_and_payloads(
592+
distinct_id,
593+
groups=groups,
594+
person_properties=person_properties,
595+
group_properties=group_properties,
596+
only_evaluate_locally=only_evaluate_locally,
597+
)
598+
return flags["featureFlags"]
599+
600+
def get_all_flags_and_payloads(
601+
self, distinct_id, *, groups={}, person_properties={}, group_properties={}, only_evaluate_locally=False
602+
):
603+
flags, payloads, fallback_to_decide = self._get_all_flags_and_payloads_locally(
604+
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
605+
)
606+
response = {"featureFlags": flags, "featureFlagPayloads": payloads}
607+
608+
if fallback_to_decide and not only_evaluate_locally:
609+
try:
610+
flags_and_payloads = self.get_decide(
611+
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
612+
)
613+
response = flags_and_payloads
614+
except Exception as e:
615+
self.log.exception(f"[FEATURE FLAGS] Unable to get feature flags and payloads: {e}")
616+
617+
return response
618+
619+
def _get_all_flags_and_payloads_locally(self, distinct_id, *, groups={}, person_properties={}, group_properties={}):
528620
require("distinct_id", distinct_id, ID_TYPES)
529621
require("groups", groups, dict)
530622

531623
if self.feature_flags == None and self.personal_api_key:
532624
self.load_feature_flags()
533625

534-
response = {}
626+
flags = {}
627+
payloads = {}
535628
fallback_to_decide = False
536-
537629
# If loading in previous line failed
538630
if self.feature_flags:
539631
for flag in self.feature_flags:
540632
try:
541-
response[flag["key"]] = self._compute_flag_locally(
633+
flags[flag["key"]] = self._compute_flag_locally(
542634
flag,
543635
distinct_id,
544636
groups=groups,
545637
person_properties=person_properties,
546638
group_properties=group_properties,
547639
)
640+
matched_payload = self._compute_payload_locally(flag["key"], flags[flag["key"]])
641+
if matched_payload:
642+
payloads[flag["key"]] = matched_payload
548643
except InconclusiveMatchError as e:
549644
# No need to log this, since it's just telling us to fall back to `/decide`
550645
fallback_to_decide = True
551646
except Exception as e:
552-
self.log.exception(f"[FEATURE FLAGS] Error while computing variant: {e}")
647+
self.log.exception(f"[FEATURE FLAGS] Error while computing variant and payload: {e}")
553648
fallback_to_decide = True
554649
else:
555650
fallback_to_decide = True
556651

557-
if fallback_to_decide and not only_evaluate_locally:
558-
try:
559-
feature_flags = self.get_feature_variants(
560-
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
561-
)
562-
response = {**response, **feature_flags}
563-
except Exception as e:
564-
self.log.exception(f"[FEATURE FLAGS] Unable to get feature variants: {e}")
565-
566-
return response
652+
return flags, payloads, fallback_to_decide
567653

568654

569655
def require(name, field, data_type):

posthog/request.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def _process_response(
6363

6464
def decide(api_key: str, host: Optional[str] = None, gzip: bool = False, timeout: int = 15, **kwargs) -> Any:
6565
"""Post the `kwargs to the decide API endpoint"""
66-
res = post(api_key, host, "/decide/?v=2", gzip, timeout, **kwargs)
66+
res = post(api_key, host, "/decide/?v=3", gzip, timeout, **kwargs)
6767
return _process_response(res, success_message="Feature flags decided successfully")
6868

6969

0 commit comments

Comments
 (0)