From 97390e74eccb0d7555813d080cfc880a75f37189 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Fri, 26 Jan 2024 15:27:26 +0000 Subject: [PATCH] compute blast radius --- posthog/api/feature_flag.py | 1 + .../test/__snapshots__/test_feature_flag.ambr | 8 ++--- posthog/api/test/test_feature_flag.py | 31 +++++++++++++++++++ .../models/feature_flag/user_blast_radius.py | 19 ++++++++++-- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/posthog/api/feature_flag.py b/posthog/api/feature_flag.py index 90bbef1d2e6d4..3673329ea32d0 100644 --- a/posthog/api/feature_flag.py +++ b/posthog/api/feature_flag.py @@ -636,6 +636,7 @@ def user_blast_radius(self, request: request.Request, **kwargs): condition = request.data.get("condition") or {} group_type_index = request.data.get("group_type_index", None) + # TODO: Handle distinct_id and $group_key properties, which are not currently supported users_affected, total_users = get_user_blast_radius(self.team, condition, group_type_index) return Response( diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index 6370cb16b9d16..ffd0e1f59188c 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -276,12 +276,12 @@ (SELECT id FROM person WHERE team_id = 2 - AND ((has(['none'], replaceRegexpAll(JSONExtractRaw(properties, 'group'), '^"|"$', '')) - OR has(['1', '2', '3'], replaceRegexpAll(JSONExtractRaw(properties, 'group'), '^"|"$', '')))) ) + AND (((has(['none'], replaceRegexpAll(JSONExtractRaw(properties, 'group'), '^"|"$', ''))) + OR (has(['1', '2', '3'], replaceRegexpAll(JSONExtractRaw(properties, 'group'), '^"|"$', ''))))) ) GROUP BY id HAVING max(is_deleted) = 0 - AND ((has(['none'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), 'group'), '^"|"$', '')) - OR has(['1', '2', '3'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), 'group'), '^"|"$', '')))) SETTINGS optimize_aggregation_in_order = 1) + AND (((has(['none'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), 'group'), '^"|"$', ''))) + OR (has(['1', '2', '3'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), 'group'), '^"|"$', ''))))) SETTINGS optimize_aggregation_in_order = 1) ''' # --- # name: TestBlastRadius.test_user_blast_radius_with_single_cohort.1 diff --git a/posthog/api/test/test_feature_flag.py b/posthog/api/test/test_feature_flag.py index 1982f5ba891db..90e2e980271a5 100644 --- a/posthog/api/test/test_feature_flag.py +++ b/posthog/api/test/test_feature_flag.py @@ -4197,6 +4197,37 @@ def test_user_blast_radius(self): response_json = response.json() self.assertDictContainsSubset({"users_affected": 4, "total_users": 10}, response_json) + @freeze_time("2024-01-11") + def test_user_blast_radius_with_relative_date_filters(self): + for i in range(8): + _create_person( + team_id=self.team.pk, + distinct_ids=[f"person{i}"], + properties={"group": f"{i}", "created_at": f"2023-0{i+1}-04"}, + ) + + response = self.client.post( + f"/api/projects/{self.team.id}/feature_flags/user_blast_radius", + { + "condition": { + "properties": [ + { + "key": "created_at", + "type": "person", + "value": "-10m", + "operator": "is_date_before", + } + ], + "rollout_percentage": 100, + } + }, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response_json = response.json() + self.assertDictContainsSubset({"users_affected": 3, "total_users": 8}, response_json) + def test_user_blast_radius_with_zero_users(self): response = self.client.post( f"/api/projects/{self.team.id}/feature_flags/user_blast_radius", diff --git a/posthog/models/feature_flag/user_blast_radius.py b/posthog/models/feature_flag/user_blast_radius.py index 5843e3513e6b1..b7e9d216b3f6c 100644 --- a/posthog/models/feature_flag/user_blast_radius.py +++ b/posthog/models/feature_flag/user_blast_radius.py @@ -7,6 +7,19 @@ from posthog.models.filters import Filter from posthog.models.property import GroupTypeIndex from posthog.models.team.team import Team +from posthog.queries.base import relative_date_parse_for_feature_flag_matching + + +def replace_proxy_properties(team: Team, feature_flag_condition: dict): + prop_groups = Filter(data=feature_flag_condition, team=team).property_groups + + for prop in prop_groups.flat: + if prop.operator in ("is_date_before", "is_date_after"): + relative_date = relative_date_parse_for_feature_flag_matching(str(prop.value)) + if relative_date: + prop.value = relative_date.strftime("%Y-%m-%d %H:%M:%S") + + return Filter(data={"properties": prop_groups.to_dict()}, team=team) def get_user_blast_radius( @@ -19,6 +32,8 @@ def get_user_blast_radius( # No rollout % calculations here, since it makes more sense to compute that on the frontend properties = feature_flag_condition.get("properties") or [] + cleaned_filter = replace_proxy_properties(team, feature_flag_condition) + if group_type_index is not None: try: from ee.clickhouse.queries.groups_join_query import GroupsJoinQuery @@ -26,7 +41,7 @@ def get_user_blast_radius( return 0, 0 if len(properties) > 0: - filter = Filter(data=feature_flag_condition, team=team) + filter = cleaned_filter for property in filter.property_groups.flat: if property.group_type_index is None or (property.group_type_index != group_type_index): @@ -50,7 +65,7 @@ def get_user_blast_radius( return total_affected_count, team.groups_seen_so_far(group_type_index) if len(properties) > 0: - filter = Filter(data=feature_flag_condition, team=team) + filter = cleaned_filter cohort_filters = [] for property in filter.property_groups.flat: if property.type in ["cohort", "precalculated-cohort", "static-cohort"]: