Skip to content

Commit

Permalink
feat: time to convert funnel udf (#29285)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
aspicer and github-actions[bot] authored Mar 7, 2025
1 parent aed5951 commit b156b1a
Show file tree
Hide file tree
Showing 8 changed files with 1,009 additions and 10 deletions.
4 changes: 0 additions & 4 deletions docker/clickhouse/user_defined_function.xml
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,6 @@
<type>UInt8</type>
<name>num_steps</name>
</argument>
<argument>
<type>UInt8</type>
<name>num_steps</name>
</argument>
<argument>
<type>UInt64</type>
<name>conversion_window_limit</name>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export const FEATURE_FLAGS = {
SETTINGS_SESSION_TABLE_VERSION: 'settings-session-table-version', // owner: @robbie-c
INSIGHT_FUNNELS_USE_UDF: 'insight-funnels-use-udf', // owner: @aspicer #team-product-analytics
INSIGHT_FUNNELS_USE_UDF_TRENDS: 'insight-funnels-use-udf-trends', // owner: @aspicer #team-product-analytics
INSIGHT_FUNNELS_USE_UDF_TIME_TO_CONVERT: 'insight-funnels-use-udf-time-to-convert', // owner: @aspicer #team-product-analytics
BATCH_EXPORTS_POSTHOG_HTTP: 'posthog-http-batch-exports',
DATA_MODELING: 'data-modeling', // owner: @EDsCODE #team-data-warehouse
HEDGEHOG_SKIN_SPIDERHOG: 'hedgehog-skin-spiderhog', // owner: @benjackwhite
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from typing import cast

from rest_framework.exceptions import ValidationError

from posthog.constants import FUNNEL_TO_STEP
from posthog.hogql import ast
from posthog.hogql.parser import parse_select
from posthog.hogql_queries.insights.funnels import FunnelUDF, FunnelTimeToConvert
from posthog.hogql_queries.insights.funnels.base import FunnelBase
from posthog.hogql_queries.insights.funnels.funnel_query_context import FunnelQueryContext
from posthog.hogql_queries.insights.funnels.utils import get_funnel_order_class
from posthog.schema import FunnelTimeToConvertResults, StepOrderValue


class FunnelTimeToConvertUDF(FunnelBase):
def __init__(
self,
context: FunnelQueryContext,
):
super().__init__(context)

self.funnel_order: FunnelUDF = get_funnel_order_class(self.context.funnelsFilter, use_udf=True)(
context=self.context
)

def _format_results(self, results: list) -> FunnelTimeToConvertResults:
return FunnelTimeToConvertResults(
bins=[[bin_from_seconds, person_count] for bin_from_seconds, person_count, _ in results],
average_conversion_time=results[0][2],
)

def get_query(self) -> ast.SelectQuery:
query, funnelsFilter = self.context.query, self.context.funnelsFilter
if funnelsFilter.funnelOrderType == StepOrderValue.UNORDERED:
# Currently don't support unordered in UDFs
return FunnelTimeToConvert(self.context).get_query()

# Conversion from which step should be calculated
from_step = funnelsFilter.funnelFromStep or 0
# Conversion to which step should be calculated
to_step = funnelsFilter.funnelToStep or len(query.series) - 1

# Use custom bin_count if provided by user, otherwise infer an automatic one based on the number of samples
binCount = funnelsFilter.binCount
if binCount is not None:
# Custom count is clamped between 1 and 90
if binCount < 1:
binCount = 1
elif binCount > 90:
binCount = 90
bin_count_expression = f"""{binCount}"""
else:
# Auto count is clamped between 1 and 60
bin_count_expression = f"""toInt(least(60, greatest(1, ceil(cbrt(ifNull(length(timings), 0))))))"""

if not (0 < to_step < len(query.series)):
raise ValidationError(
f'Filter parameter {FUNNEL_TO_STEP} can only be one of {", ".join(map(str, range(1, len(query.series))))} for time to convert!'
)

inner_select = self.funnel_order._inner_aggregation_query()

timings = parse_select(
f"""
SELECT
groupArray(arraySum(arraySlice(timings, {from_step+1}, {to_step - from_step}))) as timings,
{bin_count_expression} as bin_count,
floor(arrayMin(timings)) as min_timing,
ceil(arrayMax(timings)) as max_timing,
ceil((max_timing - min_timing) / bin_count) as bin_width_seconds_raw,
if(bin_width_seconds_raw > 0, bin_width_seconds_raw, 60) AS bin_width_seconds,
arrayMap(n -> toInt(round(min_timing + n * bin_width_seconds)), range(0, bin_count + 1)) as buckets,
arrayMap(timing -> toInt(floor((timing - min_timing) / bin_width_seconds)), timings) as indices,
arrayMap(x -> countEqual(indices, x-1), range(1, bin_count + 2)) as counts
FROM {{inner_select}}
WHERE step_reached >= {to_step}""",
{"inner_select": inner_select},
)

return cast(
ast.SelectQuery,
parse_select(
f"""
SELECT
bin_from_seconds,
person_count,
arrayAvg(timings) as averageConversionTime
FROM {{timings}}
ARRAY JOIN
counts as person_count,
buckets as bin_from_seconds
""",
{"timings": timings},
),
)
11 changes: 6 additions & 5 deletions posthog/hogql_queries/insights/funnels/funnels_query_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from posthog.hogql.timings import HogQLTimings
from posthog.hogql_queries.insights.funnels.funnel_query_context import FunnelQueryContext
from posthog.hogql_queries.insights.funnels.funnel_time_to_convert import FunnelTimeToConvert
from posthog.hogql_queries.insights.funnels.funnel_time_to_convert_udf import FunnelTimeToConvertUDF
from posthog.hogql_queries.insights.funnels.funnel_trends import FunnelTrends
from posthog.hogql_queries.insights.funnels.funnel_trends_udf import FunnelTrendsUDF
from posthog.hogql_queries.insights.funnels.utils import get_funnel_actor_class, get_funnel_order_class, use_udf
Expand Down Expand Up @@ -122,12 +123,12 @@ def funnel_class(self):
funnelVizType = self.context.funnelsFilter.funnelVizType

if funnelVizType == FunnelVizType.TRENDS:
return (
FunnelTrendsUDF(context=self.context, **self.kwargs)
if self._use_udf and self.context.funnelsFilter.funnelOrderType != StepOrderValue.UNORDERED
else FunnelTrends(context=self.context, **self.kwargs)
)
if self._use_udf and self.context.funnelsFilter.funnelOrderType != StepOrderValue.UNORDERED:
return FunnelTrendsUDF(context=self.context, **self.kwargs)
return FunnelTrends(context=self.context, **self.kwargs)
elif funnelVizType == FunnelVizType.TIME_TO_CONVERT:
if self._use_udf:
return FunnelTimeToConvertUDF(context=self.context)
return FunnelTimeToConvert(context=self.context)
else:
return self.funnel_order_class
Expand Down
Loading

0 comments on commit b156b1a

Please sign in to comment.