Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion example.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,29 @@
)


# Capture an event
# Capture an event with all feature flags
posthog.capture(
"event",
distinct_id="distinct_id",
properties={"property1": "value", "property2": "value"},
send_feature_flags=True,
)

# Capture an event with specific feature flags using flag_keys
posthog.capture(
"event-with-specific-flags",
distinct_id="distinct_id",
properties={"property1": "value", "property2": "value"},
send_feature_flags={
"only_evaluate_locally": True,
"flag_keys": [
"beta-feature",
"person-on-events-enabled",
], # Only evaluate these two flags
"person_properties": {"plan": "premium"},
},
)

print(posthog.feature_enabled("beta-feature", "distinct_id"))
print(
posthog.feature_enabled(
Expand Down
33 changes: 31 additions & 2 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ def capture(
group_properties=flag_options["group_properties"],
disable_geoip=disable_geoip,
only_evaluate_locally=True,
flag_keys=flag_options["flag_keys"],
)
else:
# Default behavior - use remote evaluation
Expand All @@ -549,6 +550,15 @@ def capture(
group_properties=flag_options["group_properties"],
disable_geoip=disable_geoip,
)

# Filter by flag_keys if provided
if flag_options["flag_keys"] is not None:
flag_keys_set = set(flag_options["flag_keys"])
feature_variants = {
key: value
for key, value in feature_variants.items()
if key in flag_keys_set
}
except Exception as e:
self.log.exception(
f"[FEATURE FLAGS] Unable to get feature variants: {e}"
Expand All @@ -561,6 +571,7 @@ def capture(
groups=(groups or {}),
disable_geoip=disable_geoip,
only_evaluate_locally=True,
flag_keys=flag_options.get("flag_keys") if flag_options else None,
)

for feature, variant in (feature_variants or {}).items():
Expand Down Expand Up @@ -588,7 +599,7 @@ def _parse_send_feature_flags(self, send_feature_flags) -> dict:

Returns:
dict: Normalized options with keys: should_send, only_evaluate_locally,
person_properties, group_properties
person_properties, group_properties, flag_keys

Raises:
TypeError: If send_feature_flags is not bool or dict
Expand All @@ -601,13 +612,15 @@ def _parse_send_feature_flags(self, send_feature_flags) -> dict:
),
"person_properties": send_feature_flags.get("person_properties"),
"group_properties": send_feature_flags.get("group_properties"),
"flag_keys": send_feature_flags.get("flag_keys"),
}
elif isinstance(send_feature_flags, bool):
return {
"should_send": send_feature_flags,
"only_evaluate_locally": None,
"person_properties": None,
"group_properties": None,
"flag_keys": None,
}
else:
raise TypeError(
Expand Down Expand Up @@ -1667,6 +1680,7 @@ def get_all_flags(
group_properties={},
only_evaluate_locally=False,
disable_geoip=None,
flag_keys=None,
) -> Optional[dict[str, Union[bool, str]]]:
"""
Get all feature flags for a user.
Expand All @@ -1678,6 +1692,7 @@ def get_all_flags(
group_properties: A dictionary of group properties.
only_evaluate_locally: Whether to only evaluate locally.
disable_geoip: Whether to disable GeoIP for this request.
flag_keys: List of specific feature flag keys to evaluate.

Examples:
```python
Expand All @@ -1694,6 +1709,7 @@ def get_all_flags(
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
disable_geoip=disable_geoip,
flag_keys=flag_keys,
)

return response["featureFlags"]
Expand All @@ -1707,6 +1723,7 @@ def get_all_flags_and_payloads(
group_properties={},
only_evaluate_locally=False,
disable_geoip=None,
flag_keys=None,
) -> FlagsAndPayloads:
"""
Get all feature flags and their payloads for a user.
Expand All @@ -1718,6 +1735,7 @@ def get_all_flags_and_payloads(
group_properties: A dictionary of group properties.
only_evaluate_locally: Whether to only evaluate locally.
disable_geoip: Whether to disable GeoIP for this request.
flag_keys: List of specific feature flag keys to evaluate.

Examples:
```python
Expand All @@ -1741,6 +1759,7 @@ def get_all_flags_and_payloads(
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
flag_keys=flag_keys,
)

if fallback_to_decide and not only_evaluate_locally:
Expand Down Expand Up @@ -1768,6 +1787,7 @@ def _get_all_flags_and_payloads_locally(
person_properties={},
group_properties={},
warn_on_unknown_groups=False,
flag_keys=None,
) -> tuple[FlagsAndPayloads, bool]:
if self.feature_flags is None and self.personal_api_key:
self.load_feature_flags()
Expand All @@ -1777,7 +1797,16 @@ def _get_all_flags_and_payloads_locally(
fallback_to_decide = False
# If loading in previous line failed
if self.feature_flags:
for flag in self.feature_flags:
flags_to_evaluate = self.feature_flags

# Filter by flag_keys if provided
if flag_keys is not None:
flag_keys_set = set(flag_keys)
flags_to_evaluate = [
flag for flag in self.feature_flags if flag["key"] in flag_keys_set
]

for flag in flags_to_evaluate:
try:
flags[flag["key"]] = self._compute_flag_locally(
flag,
Expand Down
143 changes: 143 additions & 0 deletions posthog/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,143 @@ def test_capture_with_send_feature_flags_options_default_behavior(
msg["properties"]["$feature/default-flag"], "default-value"
)

@mock.patch("posthog.client.flags")
def test_capture_with_send_feature_flags_flag_keys_filter(self, patch_flags):
"""Test that flag_keys filters the feature flags that are evaluated"""
with mock.patch("posthog.client.batch_post") as mock_post:
client = Client(
FAKE_TEST_API_KEY,
on_error=self.set_fail,
personal_api_key=FAKE_TEST_API_KEY,
sync_mode=True,
)

# Set up multiple local flags
client.feature_flags = [
{
"id": 1,
"key": "flag-one",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
],
},
},
{
"id": 2,
"key": "flag-two",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
],
},
},
{
"id": 3,
"key": "flag-three",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
],
},
},
]

# Only evaluate flag-one and flag-three
send_options = {
"only_evaluate_locally": True,
"flag_keys": ["flag-one", "flag-three"],
}

msg_uuid = client.capture(
"test event", distinct_id="distinct_id", send_feature_flags=send_options
)

self.assertIsNotNone(msg_uuid)
self.assertFalse(self.failed)

# Check the message only includes flag-one and flag-three
mock_post.assert_called_once()
batch_data = mock_post.call_args[1]["batch"]
msg = batch_data[0]

# Should have flag-one and flag-three, but not flag-two
self.assertEqual(msg["properties"]["$feature/flag-one"], True)
self.assertEqual(msg["properties"]["$feature/flag-three"], True)
self.assertNotIn("$feature/flag-two", msg["properties"])

# Active flags should only include flag-one and flag-three
self.assertEqual(
sorted(msg["properties"]["$active_feature_flags"]),
["flag-one", "flag-three"],
)

@mock.patch("posthog.client.flags")
def test_capture_with_send_feature_flags_flag_keys_remote_evaluation(
self, patch_flags
):
"""Test that flag_keys filters remote evaluation results"""
# Mock remote flags response with multiple flags
patch_flags.return_value = {
"featureFlags": {
"remote-flag-one": "value-one",
"remote-flag-two": "value-two",
"remote-flag-three": "value-three",
}
}

with mock.patch("posthog.client.batch_post") as mock_post:
client = Client(
FAKE_TEST_API_KEY,
on_error=self.set_fail,
sync_mode=True,
)

# Only evaluate remote-flag-one and remote-flag-three
send_options = {
"flag_keys": ["remote-flag-one", "remote-flag-three"],
}

msg_uuid = client.capture(
"test event", distinct_id="distinct_id", send_feature_flags=send_options
)

self.assertIsNotNone(msg_uuid)
self.assertFalse(self.failed)

# Verify flags() was called
patch_flags.assert_called_once()

# Check the message only includes remote-flag-one and remote-flag-three
mock_post.assert_called_once()
batch_data = mock_post.call_args[1]["batch"]
msg = batch_data[0]

# Should have remote-flag-one and remote-flag-three, but not remote-flag-two
self.assertEqual(msg["properties"]["$feature/remote-flag-one"], "value-one")
self.assertEqual(
msg["properties"]["$feature/remote-flag-three"], "value-three"
)
self.assertNotIn("$feature/remote-flag-two", msg["properties"])

# Active flags should only include remote-flag-one and remote-flag-three
self.assertEqual(
sorted(msg["properties"]["$active_feature_flags"]),
["remote-flag-one", "remote-flag-three"],
)

@mock.patch("posthog.client.flags")
def test_capture_exception_with_send_feature_flags_options(self, patch_flags):
"""Test that capture_exception also supports SendFeatureFlagsOptions"""
Expand Down Expand Up @@ -2185,6 +2322,7 @@ def test_parse_send_feature_flags_method(self):
"only_evaluate_locally": None,
"person_properties": None,
"group_properties": None,
"flag_keys": None,
}
self.assertEqual(result, expected)

Expand All @@ -2195,6 +2333,7 @@ def test_parse_send_feature_flags_method(self):
"only_evaluate_locally": None,
"person_properties": None,
"group_properties": None,
"flag_keys": None,
}
self.assertEqual(result, expected)

Expand All @@ -2203,13 +2342,15 @@ def test_parse_send_feature_flags_method(self):
"only_evaluate_locally": True,
"person_properties": {"plan": "premium"},
"group_properties": {"company": {"type": "enterprise"}},
"flag_keys": ["beta-feature", "my-flag"],
}
result = client._parse_send_feature_flags(options)
expected = {
"should_send": True,
"only_evaluate_locally": True,
"person_properties": {"plan": "premium"},
"group_properties": {"company": {"type": "enterprise"}},
"flag_keys": ["beta-feature", "my-flag"],
}
self.assertEqual(result, expected)

Expand All @@ -2221,6 +2362,7 @@ def test_parse_send_feature_flags_method(self):
"only_evaluate_locally": None,
"person_properties": {"user_id": "123"},
"group_properties": None,
"flag_keys": None,
}
self.assertEqual(result, expected)

Expand All @@ -2231,6 +2373,7 @@ def test_parse_send_feature_flags_method(self):
"only_evaluate_locally": None,
"person_properties": None,
"group_properties": None,
"flag_keys": None,
}
self.assertEqual(result, expected)

Expand Down
3 changes: 3 additions & 0 deletions posthog/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ class SendFeatureFlagsOptions(TypedDict, total=False):
These properties will be merged with any existing person properties.
group_properties: Group properties to use for feature flag evaluation specific to this event.
Format: { group_type_name: { group_properties } }
flag_keys: List of specific feature flag keys to evaluate. Only these flags will be evaluated
and included in the event. If not provided, all flags will be evaluated.
"""

only_evaluate_locally: Optional[bool]
person_properties: Optional[dict[str, Any]]
group_properties: Optional[dict[str, dict[str, Any]]]
flag_keys: Optional[list[str]]


@dataclass(frozen=True)
Expand Down
Loading