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

fix(hogql): fix edge cases when converting real world lifecycle filters #17728

Closed
wants to merge 5 commits into from
Closed
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
78 changes: 69 additions & 9 deletions posthog/hogql_queries/legacy_compatibility/filter_to_query.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import List, Dict
from posthog.models.entity.entity import Entity as BackendEntity
from posthog.models.filters import AnyInsightFilter
from posthog.models.filters.filter import Filter as LegacyFilter
Expand Down Expand Up @@ -27,17 +28,72 @@
from posthog.types import InsightQueryNode


def entity_to_node(entity: BackendEntity) -> EventsNode | ActionsNode:
def is_property_with_operator(property: Dict):
Copy link
Member

Choose a reason for hiding this comment

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

Note that the frontend has isPropertyFilterWithOperator, but it works differently

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! I actually forgot we have something similar on the frontend. Will adapt the naming in the follow up PR, as this got superseded by work in #17813.

return property.get("type") not in ("cohort", "hogql")


def clean_property(property: Dict):
cleaned_property = {**property}

# convert precalculated cohorts to cohorts
if cleaned_property.get("type") == "precalculated-cohort":
cleaned_property["type"] = "cohort"

# set a default operator for properties that support it, but don't have an operator set
if is_property_with_operator(cleaned_property) and cleaned_property.get("operator") is None:
cleaned_property["operator"] = "exact"

# remove the operator for properties that don't support it, but have it set
if not is_property_with_operator(cleaned_property) and cleaned_property.get("operator") is not None:
del cleaned_property["operator"]

# remove keys without concrete value
cleaned_property = {key: value for key, value in cleaned_property.items() if value is not None}

return cleaned_property


def clean_entity_properties(properties: List[Dict] | None):
if properties is None:
return None
else:
return list(map(clean_property, properties))


def clean_property_group_filter_value(value: Dict):
if value.get("type") in ("AND", "OR"):
value["values"] = map(clean_property_group_filter_value, value.get("values"))
return value
else:
return clean_property(value)


def clean_properties(properties: Dict):
properties["values"] = map(clean_property_group_filter_value, properties.get("values"))
return properties


def entity_to_node(entity: BackendEntity, include_properties: bool, include_math: bool) -> EventsNode | ActionsNode:
shared = {
"name": entity.name,
"custom_name": entity.custom_name,
"properties": entity._data.get("properties", None),
"math": entity.math,
"math_property": entity.math_property,
"math_hogql": entity.math_hogql,
"math_group_type_index": entity.math_group_type_index,
}

if include_properties:
shared = {
**shared,
"properties": clean_entity_properties(entity._data.get("properties", None)),
}

if include_math:
shared = {
**shared,
"math": entity.math,
"math_property": entity.math_property,
"math_hogql": entity.math_hogql,
"math_group_type_index": entity.math_group_type_index,
}

if entity.type == "actions":
return ActionsNode(id=entity.id, **shared)
else:
Expand Down Expand Up @@ -75,9 +131,13 @@ def _interval(filter: AnyInsightFilter):


def _series(filter: AnyInsightFilter):
include_math = True
include_properties = True
if filter.insight == "RETENTION" or filter.insight == "PATHS":
return {}
return {"series": map(entity_to_node, filter.entities)}
elif filter.insight == "LIFECYCLE":
include_math = False
return {"series": map(lambda entity: entity_to_node(entity, include_properties, include_math), filter.entities)}


def _sampling_factor(filter: AnyInsightFilter):
Expand All @@ -94,9 +154,9 @@ def _properties(filter: AnyInsightFilter):
return {}
elif isinstance(raw_properties, list):
raw_properties = {"type": "AND", "values": [{"type": "AND", "values": raw_properties}]}
return {"properties": PropertyGroupFilter(**raw_properties)}
return {"properties": PropertyGroupFilter(**clean_properties(raw_properties))}
else:
return {"properties": PropertyGroupFilter(**raw_properties)}
return {"properties": PropertyGroupFilter(**clean_properties(raw_properties))}


def _breakdown_filter(filter: AnyInsightFilter):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,85 @@
"filter_test_accounts": True,
}

# real world regression tests

insight_18 = {
"actions": [
{
"id": 2760,
"math": "total",
"name": "Pageviews",
"type": "actions",
"order": 0,
"properties": [{"key": "$browser", "type": "event", "value": "Chrome", "operator": None}],
"math_property": None,
}
],
"display": "ActionsBar",
"insight": "LIFECYCLE",
"interval": "day",
"shown_as": "Lifecycle",
}

insight_19 = {
"events": [
{
"id": "created change",
"math": "total",
"name": "created change",
"type": "events",
"order": 0,
"properties": [{"key": "id", "type": "cohort", "value": 2208, "operator": None}],
"custom_name": None,
"math_property": None,
}
],
"display": "ActionsLineGraph",
"insight": "LIFECYCLE",
"interval": "day",
"shown_as": "Lifecycle",
}

insight_20 = {
"events": [
{
"id": "$pageview",
"math": "total",
"name": "$pageview",
"type": "events",
"order": 0,
"properties": [],
"math_property": None,
}
],
"display": "ActionsLineGraph",
"insight": "LIFECYCLE",
"interval": "day",
"shown_as": "Lifecycle",
"properties": [{"key": "id", "type": "cohort", "value": 929, "operator": "exact"}],
}

insight_21 = {
"actions": [
{
"id": 4317,
"math": "total",
"name": "Used newer version of the app",
"type": "actions",
"order": 0,
"properties": [],
"custom_name": None,
"math_property": None,
}
],
"display": "ActionsLineGraph",
"insight": "LIFECYCLE",
"interval": "day",
"shown_as": "Lifecycle",
"properties": [{"key": "id", "type": "precalculated-cohort", "value": 760, "operator": None}],
"funnel_window_days": 14,
}

test_insights = [
insight_0,
insight_1,
Expand All @@ -319,6 +398,10 @@
insight_15,
insight_16,
insight_17,
insight_18,
insight_19,
insight_20,
insight_21,
]


Expand Down Expand Up @@ -405,6 +488,7 @@ def test_base_insights(insight):
{"type": "OR", "values": [{}]},
],
}
properties_10 = [{"key": "id", "type": "cohort", "value": 71, "operator": None}]

test_properties = [
properties_0,
Expand All @@ -417,6 +501,7 @@ def test_base_insights(insight):
properties_7,
properties_8,
properties_9,
properties_10,
]


Expand Down