Skip to content

Commit

Permalink
feat(hogql): add funnel breakdowns (#20153)
Browse files Browse the repository at this point in the history
  • Loading branch information
thmsobrmlr authored Feb 12, 2024
1 parent 5bb9cf9 commit f663235
Show file tree
Hide file tree
Showing 10 changed files with 4,938 additions and 134 deletions.
4 changes: 3 additions & 1 deletion posthog/hogql/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,8 +1075,10 @@ def visit_window_expr(self, node: ast.WindowExpr):
if len(node.partition_by) == 0:
raise HogQLException("PARTITION BY must have at least one argument")
strings.append("PARTITION BY")
columns = []
for expr in node.partition_by:
strings.append(self.visit(expr))
columns.append(self.visit(expr))
strings.append(", ".join(columns))

if node.order_by is not None:
if len(node.order_by) == 0:
Expand Down
499 changes: 407 additions & 92 deletions posthog/hogql_queries/insights/funnels/base.py

Large diffs are not rendered by default.

24 changes: 13 additions & 11 deletions posthog/hogql_queries/insights/funnels/funnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Funnel(FunnelBase):
def get_query(self):
max_steps = self.context.max_steps

breakdown_exprs = self._get_breakdown_expr()
breakdown_exprs = self._get_breakdown_prop_expr()

select: List[ast.Expr] = [
*self._get_count_columns(max_steps),
Expand All @@ -50,7 +50,7 @@ def get_query(self):

def get_step_counts_query(self):
max_steps = self.context.max_steps
breakdown_exprs = self._get_breakdown_expr()
breakdown_exprs = self._get_breakdown_prop_expr()
inner_timestamps, outer_timestamps = self._get_timestamp_selects()
person_and_group_properties = self._get_person_and_group_properties()

Expand Down Expand Up @@ -104,7 +104,7 @@ def get_step_counts_without_aggregation_query(self):
raise ValidationError("Funnels require at least two steps before calculating.")

formatted_query = self._build_step_subquery(2, max_steps)
breakdown_exprs = self._get_breakdown_expr()
breakdown_exprs = self._get_breakdown_prop_expr()

select: List[ast.Expr] = [
ast.Field(chain=["*"]),
Expand All @@ -120,11 +120,13 @@ def get_step_counts_without_aggregation_query(self):
ast.CompareOperation(
left=ast.Field(chain=["step_0"]), right=ast.Constant(value=1), op=ast.CompareOperationOp.Eq
),
ast.CompareOperation(
left=ast.Field(chain=["exclusion"]), right=ast.Constant(value=0), op=ast.CompareOperationOp.Eq
)
if self._get_exclusion_condition() != []
else None,
(
ast.CompareOperation(
left=ast.Field(chain=["exclusion"]), right=ast.Constant(value=0), op=ast.CompareOperationOp.Eq
)
if self._get_exclusion_condition() != []
else None
),
]
where = ast.And(exprs=[expr for expr in where_exprs if expr is not None])

Expand All @@ -142,7 +144,7 @@ def _build_step_subquery(
select = [
*select,
*self._get_partition_cols(1, max_steps),
*self._get_breakdown_expr(group_remaining=True),
*self._get_breakdown_prop_expr(group_remaining=True),
*self._get_person_and_group_properties(),
]

Expand All @@ -153,13 +155,13 @@ def _build_step_subquery(
outer_select = [
*select,
*self._get_partition_cols(level_index, max_steps),
*self._get_breakdown_expr(),
*self._get_breakdown_prop_expr(),
*self._get_person_and_group_properties(),
]
inner_select = [
*select,
*self._get_comparison_cols(level_index, max_steps),
*self._get_breakdown_expr(),
*self._get_breakdown_prop_expr(),
*self._get_person_and_group_properties(),
]

Expand Down
44 changes: 43 additions & 1 deletion posthog/hogql_queries/insights/funnels/funnel_query_context.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from typing import Optional
from typing import List, Optional, Union
from posthog.hogql.constants import LimitContext
from posthog.hogql.timings import HogQLTimings
from posthog.hogql_queries.insights.query_context import QueryContext
from posthog.models.filters.mixins.utils import cached_property
from posthog.models.property.util import box_value
from posthog.models.team.team import Team
from posthog.schema import (
BreakdownAttributionType,
BreakdownFilter,
BreakdownType,
FunnelConversionWindowTimeUnit,
FunnelsFilter,
FunnelsQuery,
Expand All @@ -18,6 +21,10 @@ class FunnelQueryContext(QueryContext):
funnelsFilter: FunnelsFilter
breakdownFilter: BreakdownFilter

breakdown: List[Union[str, int]] | None
breakdownType: BreakdownType
breakdownAttributionType: BreakdownAttributionType

funnelWindowInterval: int
funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit

Expand All @@ -34,11 +41,46 @@ def __init__(
self.funnelsFilter = self.query.funnelsFilter or FunnelsFilter()
self.breakdownFilter = self.query.breakdownFilter or BreakdownFilter()

# defaults
self.breakdownType = self.breakdownFilter.breakdown_type or BreakdownType.event
self.breakdownAttributionType = (
self.funnelsFilter.breakdownAttributionType or BreakdownAttributionType.first_touch
)
self.funnelWindowInterval = self.funnelsFilter.funnelWindowInterval or 14
self.funnelWindowIntervalUnit = (
self.funnelsFilter.funnelWindowIntervalUnit or FunnelConversionWindowTimeUnit.day
)

# the API accepts either:
# a string (single breakdown) in parameter "breakdown"
# a list of numbers (one or more cohorts) in parameter "breakdown"
# a list of strings (multiple breakdown) in parameter "breakdowns"
# if the breakdown is a string, box it as a list to reduce paths through the code
#
# The code below ensures that breakdown is always an array
# without it affecting the multiple areas of the code outside of funnels that use breakdown
#
# Once multi property breakdown is implemented in Trends this becomes unnecessary

# if isinstance(self._filter.breakdowns, List) and self._filter.breakdown_type in [
# "person",
# "event",
# "hogql",
# None,
# ]:
# data.update({"breakdown": [b.get("property") for b in self._filter.breakdowns]})

if isinstance(self.breakdownFilter.breakdown, str) and self.breakdownType in [
"person",
"event",
"hogql",
None,
]:
boxed_breakdown: List[Union[str, int]] = box_value(self.breakdownFilter.breakdown)
self.breakdown = boxed_breakdown
else:
self.breakdown = self.breakdownFilter.breakdown # type: ignore

@cached_property
def max_steps(self) -> int:
return len(self.query.series)
Loading

0 comments on commit f663235

Please sign in to comment.