Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(feature-flags): JSON payload function #81

Merged
merged 17 commits into from
Jan 31, 2023
5 changes: 4 additions & 1 deletion example.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
)
)

exit()

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

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

# get payload
print(posthog.get_feature_flag_payload("beta-feature", "distinct_id"))
print(posthog.get_all_flags_and_payloads("distinct_id"))
exit()
# # Alias a previous distinct id with a new one

posthog.alias("distinct_id", "new_distinct_id")
Expand Down
40 changes: 40 additions & 0 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,46 @@ def get_all_flags(
)


def get_feature_flag_payload(
key,
distinct_id,
match_value=None,
groups={},
person_properties={},
group_properties={},
only_evaluate_locally=False,
send_feature_flag_events=True,
):
return _proxy(
"get_feature_flag_payload",
key=key,
distinct_id=distinct_id,
match_value=match_value,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
send_feature_flag_events=send_feature_flag_events,
)


def get_all_flags_and_payloads(
distinct_id,
groups={},
person_properties={},
group_properties={},
only_evaluate_locally=False,
):
return _proxy(
"get_all_flags_and_payloads",
distinct_id=distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
)


def page(*args, **kwargs):
"""Send a page call."""
_proxy("page", *args, **kwargs)
Expand Down
118 changes: 102 additions & 16 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(
self.gzip = gzip
self.timeout = timeout
self.feature_flags = None
self.feature_flags_by_key = None
self.group_type_mapping = None
self.poll_interval = poll_interval
self.poller = None
Expand Down Expand Up @@ -127,6 +128,14 @@ def identify(self, distinct_id=None, properties=None, context=None, timestamp=No
return self._enqueue(msg)

def get_feature_variants(self, distinct_id, groups=None, person_properties=None, group_properties=None):
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties)
return resp_data["featureFlags"]

def get_feature_payloads(self, distinct_id, groups=None, person_properties=None, group_properties=None):
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties)
return resp_data["featureFlagPayloads"]

def get_decide(self, distinct_id, groups=None, person_properties=None, group_properties=None):
require("distinct_id", distinct_id, ID_TYPES)

if groups:
Expand All @@ -141,7 +150,7 @@ def get_feature_variants(self, distinct_id, groups=None, person_properties=None,
"group_properties": group_properties,
}
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
return resp_data["featureFlags"]
return resp_data

def capture(
self,
Expand Down Expand Up @@ -358,10 +367,18 @@ def shutdown(self):

def _load_feature_flags(self):
try:

response = get(
self.personal_api_key, f"/api/feature_flag/local_evaluation/?token={self.api_key}", self.host
self.personal_api_key,
f"/api/feature_flag/local_evaluation/?token={self.api_key}",
self.host,
timeout=10,
)

self.feature_flags = response["flags"] or []
self.feature_flags_by_key = {
flag["key"]: flag for flag in self.feature_flags if flag.get("key") is not None
}
self.group_type_mapping = response["group_type_mapping"] or {}

except APIError as e:
Expand Down Expand Up @@ -522,48 +539,117 @@ def get_feature_flag(
self.distinct_ids_feature_flags_reported[distinct_id].add(feature_flag_reported_key)
return response

def get_feature_flag_payload(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to go in __init__.py for the interface to be available.

Also add to example.py to test locally that everything works :)

self,
key,
distinct_id,
*,
match_value=None,
groups={},
person_properties={},
group_properties={},
only_evaluate_locally=False,
send_feature_flag_events=True,
):
if match_value is None:
match_value = self.get_feature_flag(
key,
distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
send_feature_flag_events=send_feature_flag_events,
only_evaluate_locally=True,
)

response = None

if match_value is not None:
response = self._compute_payload_locally(key, match_value)

if response is None and not only_evaluate_locally:
decide_payloads = self.get_feature_payloads(distinct_id, groups, person_properties, group_properties)
response = decide_payloads.get(str(key).lower(), None)

return response

def _compute_payload_locally(self, key, match_value):
payload = None

if self.feature_flags_by_key is None:
return payload

flag_definition = self.feature_flags_by_key.get(key) or {}
flag_filters = flag_definition.get("filters") or {}
flag_payloads = flag_filters.get("payloads") or {}
payload = flag_payloads.get(str(match_value).lower(), None)
return payload

def get_all_flags(
self, distinct_id, *, groups={}, person_properties={}, group_properties={}, only_evaluate_locally=False
):
flags = self.get_all_flags_and_payloads(
distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
)
return flags["featureFlags"]

def get_all_flags_and_payloads(
self, distinct_id, *, groups={}, person_properties={}, group_properties={}, only_evaluate_locally=False
):
flags, payloads, fallback_to_decide = self._get_all_flags_and_payloads_locally(
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
)
response = {"featureFlags": flags, "featureFlagPayloads": payloads}

if fallback_to_decide and not only_evaluate_locally:
try:
flags_and_payloads = self.get_decide(
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
)
response = flags_and_payloads
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Unable to get feature flags and payloads: {e}")

return response

def _get_all_flags_and_payloads_locally(self, distinct_id, *, groups={}, person_properties={}, group_properties={}):
require("distinct_id", distinct_id, ID_TYPES)
require("groups", groups, dict)

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

response = {}
flags = {}
payloads = {}
fallback_to_decide = False

# If loading in previous line failed
if self.feature_flags:
for flag in self.feature_flags:
try:
response[flag["key"]] = self._compute_flag_locally(
flags[flag["key"]] = self._compute_flag_locally(
flag,
distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
)
matched_payload = self._compute_payload_locally(flag["key"], flags[flag["key"]])
if matched_payload:
payloads[flag["key"]] = matched_payload
except InconclusiveMatchError as e:
# No need to log this, since it's just telling us to fall back to `/decide`
fallback_to_decide = True
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Error while computing variant: {e}")
self.log.exception(f"[FEATURE FLAGS] Error while computing variant and payload: {e}")
fallback_to_decide = True
else:
fallback_to_decide = True

if fallback_to_decide and not only_evaluate_locally:
try:
feature_flags = self.get_feature_variants(
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
)
response = {**response, **feature_flags}
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Unable to get feature variants: {e}")

return response
return flags, payloads, fallback_to_decide


def require(name, field, data_type):
Expand Down
2 changes: 1 addition & 1 deletion posthog/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def _process_response(

def decide(api_key: str, host: Optional[str] = None, gzip: bool = False, timeout: int = 15, **kwargs) -> Any:
"""Post the `kwargs to the decide API endpoint"""
res = post(api_key, host, "/decide/?v=2", gzip, timeout, **kwargs)
res = post(api_key, host, "/decide/?v=3", gzip, timeout, **kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the resiliency stuff in a separate pr? 😅 (I'm hoping you're handling that too 😬 )

Copy link
Member Author

@EDsCODE EDsCODE Jan 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, what resiliency needs to be added here. All the 200 checking is handled within get. We don't save decide responses so there's no upserting. It looks like all that needed to be added was the timeout of 10 seconds

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me!

return _process_response(res, success_message="Feature flags decided successfully")


Expand Down
Loading