From 65ec5f60a488cddbe444bf13b9dabda404c19ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Thu, 22 Feb 2024 14:08:34 +0100 Subject: [PATCH 01/17] feat(hogql): implement include_recordings for funnel actors --- .../hogql_queries/insights/funnels/base.py | 92 +++++++++---------- .../insights/funnels/funnel_trends.py | 11 +-- 2 files changed, 48 insertions(+), 55 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index ee0e5a6e7da82..27acecfb92587 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -45,10 +45,9 @@ def __init__(self, context: FunnelQueryContext): self._extra_event_fields: List[ColumnName] = [] self._extra_event_properties: List[PropertyName] = [] - # TODO: implement with actors query - # if self._filter.include_recordings: - # self._extra_event_fields = ["uuid"] - # self._extra_event_properties = ["$session_id", "$window_id"] + if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + self._extra_event_fields = ["uuid"] + self._extra_event_properties = ["$session_id", "$window_id"] def get_query(self) -> ast.SelectQuery: raise NotImplementedError() @@ -620,22 +619,22 @@ def _get_funnel_person_step_condition(self) -> ast.Expr: return ast.And(exprs=conditions) def _get_funnel_person_step_events(self) -> List[ast.Expr]: + if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + step_num = self.context.actorsQuery.funnelStep + # if self._filter.include_final_matching_events: + if False: # TODO: Implement with correlations + # Always returns the user's final step of the funnel + return [parse_expr("final_matching_events as matching_events")] + elif step_num is None: + raise ValueError("Missing funnelStep actors query property") + if step_num >= 0: + # None drop off case + matching_events_step_num = step_num - 1 + else: + # Drop off case if negative number + matching_events_step_num = abs(step_num) - 2 + return [parse_expr(f"step_{matching_events_step_num}_matching_events as matching_events")] return [] - # if self._filter.include_recordings: - # step_num = self._filter.funnel_step - # if self._filter.include_final_matching_events: - # # Always returns the user's final step of the funnel - # return ", final_matching_events as matching_events" - # elif step_num is None: - # raise ValueError("Missing funnel_step filter property") - # if step_num >= 0: - # # None drop off case - # self.params.update({"matching_events_step_num": step_num - 1}) - # else: - # # Drop off case if negative number - # self.params.update({"matching_events_step_num": abs(step_num) - 2}) - # return ", step_%(matching_events_step_num)s_matching_events as matching_events" - # return "" def _get_count_columns(self, max_steps: int) -> List[ast.Expr]: exprs: List[ast.Expr] = [] @@ -653,41 +652,36 @@ def _get_step_time_names(self, max_steps: int) -> List[ast.Expr]: return exprs - # def _get_final_matching_event(self, max_steps: int): - # statement = None - # for i in range(max_steps - 1, -1, -1): - # if i == max_steps - 1: - # statement = f"if(isNull(latest_{i}),step_{i-1}_matching_event,step_{i}_matching_event)" - # elif i == 0: - # statement = f"if(isNull(latest_0),(null,null,null,null),{statement})" - # else: - # statement = f"if(isNull(latest_{i}),step_{i-1}_matching_event,{statement})" - # return f",{statement} as final_matching_event" if statement else "" + def _get_final_matching_event(self, max_steps: int) -> List[ast.Expr]: + statement = None + for i in range(max_steps - 1, -1, -1): + if i == max_steps - 1: + statement = f"if(isNull(latest_{i}),step_{i-1}_matching_event,step_{i}_matching_event)" + elif i == 0: + statement = f"if(isNull(latest_0),(null,null,null,null),{statement})" + else: + statement = f"if(isNull(latest_{i}),step_{i-1}_matching_event,{statement})" + return [parse_expr(f"{statement} as final_matching_event")] if statement else [] def _get_matching_events(self, max_steps: int) -> List[ast.Expr]: - # if self._filter.include_recordings: - # events = [] - # for i in range(0, max_steps): - # event_fields = ["latest"] + self.extra_event_fields_and_properties - # event_fields_with_step = ", ".join([f'"{field}_{i}"' for field in event_fields]) - # event_clause = f"({event_fields_with_step}) as step_{i}_matching_event" - # events.append(event_clause) - # matching_event_select_statements = "," + ", ".join(events) - - # final_matching_event_statement = self._get_final_matching_event(max_steps) - - # return matching_event_select_statements + final_matching_event_statement - + if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + events = [] + for i in range(0, max_steps): + event_fields = ["latest"] + self.extra_event_fields_and_properties + event_fields_with_step = ", ".join([f'"{field}_{i}"' for field in event_fields]) + event_clause = f"({event_fields_with_step}) as step_{i}_matching_event" + events.append(parse_expr(event_clause)) + + return [*events, *self._get_final_matching_event(max_steps)] return [] def _get_matching_event_arrays(self, max_steps: int) -> List[ast.Expr]: - # select_clause = "" - # if self._filter.include_recordings: - # for i in range(0, max_steps): - # select_clause += f", groupArray(10)(step_{i}_matching_event) as step_{i}_matching_events" - # select_clause += f", groupArray(10)(final_matching_event) as final_matching_events" - # return select_clause - return [] + exprs: List[ast.Expr] = [] + if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + for i in range(0, max_steps): + exprs.append(parse_expr(f"groupArray(10)(step_{i}_matching_event) as step_{i}_matching_events")) + exprs.append(parse_expr(f"groupArray(10)(final_matching_event) as final_matching_events")) + return exprs def _get_step_time_avgs(self, max_steps: int, inner_query: bool = False) -> List[ast.Expr]: exprs: List[ast.Expr] = [] diff --git a/posthog/hogql_queries/insights/funnels/funnel_trends.py b/posthog/hogql_queries/insights/funnels/funnel_trends.py index fc07841a6de4e..9b552ad298ac9 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_trends.py +++ b/posthog/hogql_queries/insights/funnels/funnel_trends.py @@ -268,14 +268,13 @@ def get_query(self) -> ast.SelectQuery: def get_step_counts_without_aggregation_query( self, *, specific_entrance_period_start: Optional[datetime] = None ) -> ast.SelectQuery: - team, interval = self.context.team, self.context.interval + team, interval, max_steps = self.context.team, self.context.interval, self.context.max_steps steps_per_person_query = self.funnel_order.get_step_counts_without_aggregation_query() - # event_select_clause = "" - # if self._filter.include_recordings: - # max_steps = len(self._filter.entities) - # event_select_clause = self._get_matching_event_arrays(max_steps) + event_select_clause: List[ast.Expr] = [] + if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + event_select_clause = self._get_matching_event_arrays(max_steps) breakdown_clause = self._get_breakdown_prop_expr() @@ -283,7 +282,7 @@ def get_step_counts_without_aggregation_query( ast.Field(chain=["aggregation_target"]), ast.Alias(alias="entrance_period_start", expr=get_start_of_interval_hogql(interval.value, team=team)), parse_expr("max(steps) AS steps_completed"), - # {event_select_clause} + *event_select_clause, *breakdown_clause, ] select_from = ast.JoinExpr(table=steps_per_person_query) From 281a64e95eb53dbfdcf37d7443e776174244a3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Mon, 26 Feb 2024 21:37:34 +0100 Subject: [PATCH 02/17] wip --- posthog/hogql/database/schema/events.py | 1 + .../hogql_queries/insights/funnels/base.py | 44 ++-- .../hogql_queries/insights/funnels/funnel.py | 13 +- .../insights/funnels/funnel_event_query.py | 15 ++ .../insights/funnels/funnel_strict.py | 16 +- .../funnels/test/test_funnel_persons.py | 230 +++++++++--------- 6 files changed, 182 insertions(+), 137 deletions(-) diff --git a/posthog/hogql/database/schema/events.py b/posthog/hogql/database/schema/events.py index d3f32c2e1eb1c..5a4a7f132af80 100644 --- a/posthog/hogql/database/schema/events.py +++ b/posthog/hogql/database/schema/events.py @@ -67,6 +67,7 @@ class EventsTable(Table): "elements_chain": StringDatabaseField(name="elements_chain"), "created_at": DateTimeDatabaseField(name="created_at"), "$session_id": StringDatabaseField(name="$session_id"), + "$window_id": StringDatabaseField(name="$window_id"), # Lazy table that adds a join to the persons table "pdi": LazyJoin( from_field="distinct_id", diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index 27acecfb92587..fd94395dd999f 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -394,12 +394,18 @@ def _get_inner_event_query( ) entities_to_use = entities or query.series - # extra_fields = [] + extra_fields = [] # for prop in self._include_properties: # extra_fields.append(prop) - funnel_events_query = FunnelEventQuery(context=self.context).to_query(skip_entity_filter=skip_entity_filter) + funnel_events_query = FunnelEventQuery( + context=self.context, + extra_fields=[*self._extra_event_fields, *extra_fields], + extra_event_properties=self._extra_event_properties, + ).to_query( + skip_entity_filter=skip_entity_filter, + ) # funnel_events_query, params = FunnelEventQuery( # extra_fields=[*self._extra_event_fields, *extra_fields], # extra_event_properties=self._extra_event_properties, @@ -544,8 +550,10 @@ def _get_step_col( parse_expr(f"if({step_prefix}step_{index} = 1, timestamp, null) as {step_prefix}latest_{index}") ) - # for field in self.extra_event_fields_and_properties: - # step_cols.append(f'if({step_prefix}step_{index} = 1, "{field}", null) as "{step_prefix}{field}_{index}"') + for field in self.extra_event_fields_and_properties: + step_cols.append( + parse_expr(f'if({step_prefix}step_{index} = 1, "{field}", null) as "{step_prefix}{field}_{index}"') + ) return step_cols @@ -712,12 +720,18 @@ def _get_timestamp_selects(self) -> Tuple[List[ast.Expr], List[ast.Expr]]: Returns timestamp selectors for the target step and optionally the preceding step. In the former case, always returns the timestamp for the first and last step as well. """ - # target_step = self._filter.funnel_step # TODO: implement with actors - # final_step = self.context.max_steps - 1 + # actorsQuery, max_steps = ( + # self.context.actorsQuery, + # self.context.max_steps, + # ) + # assert actorsQuery is not None + + # target_step = actorsQuery.funnelStep + # final_step = max_steps - 1 # first_step = 0 # if not target_step: - # return "", "" + # return [], [] # if target_step < 0: # # the first valid dropoff argument for funnel_step is -2 @@ -743,7 +757,7 @@ def _get_timestamp_selects(self) -> Tuple[List[ast.Expr], List[ast.Expr]]: # f", argMax(latest_{target_step}, steps) as timestamp, argMax(latest_{final_step}, steps) as final_timestamp, argMax(latest_{first_step}, steps) as first_timestamp", # ) # else: - # return "", "" + # return [], [] return [], [] def _get_step_times(self, max_steps: int) -> List[ast.Expr]: @@ -774,8 +788,8 @@ def _get_partition_cols(self, level_index: int, max_steps: int) -> List[ast.Expr if i < level_index: exprs.append(ast.Field(chain=[f"latest_{i}"])) - # for field in self.extra_event_fields_and_properties: - # exprs.append(ast.Field(chain=[f'"{field}_{i}"'])) + for field in self.extra_event_fields_and_properties: + exprs.append(ast.Field(chain=[f'"{field}_{i}"'])) for exclusion_id, exclusion in enumerate(exclusions or []): if cast(int, exclusion.funnelFromStep) + 1 == i: @@ -793,10 +807,12 @@ def _get_partition_cols(self, level_index: int, max_steps: int) -> List[ast.Expr ) ) - # for field in self.extra_event_fields_and_properties: - # cols.append( - # f'last_value("{field}_{i}") over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND {duplicate_event} PRECEDING) "{field}_{i}"' - # ) + for field in self.extra_event_fields_and_properties: + exprs.append( + parse_expr( + f'last_value("{field}_{i}") over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND {duplicate_event} PRECEDING) "{field}_{i}"' + ) + ) for exclusion_id, exclusion in enumerate(exclusions or []): # exclusion starting at step i follows semantics of step i+1 in the query (since we're looking for exclusions after step i) diff --git a/posthog/hogql_queries/insights/funnels/funnel.py b/posthog/hogql_queries/insights/funnels/funnel.py index adbfaaaccc991..7982dcc4b6ea0 100644 --- a/posthog/hogql_queries/insights/funnels/funnel.py +++ b/posthog/hogql_queries/insights/funnels/funnel.py @@ -190,8 +190,8 @@ def _get_comparison_cols(self, level_index: int, max_steps: int) -> List[ast.Exp if i < level_index: exprs.append(ast.Field(chain=[f"latest_{i}"])) - # for field in self.extra_event_fields_and_properties: - # exprs.append(ast.Field(chain=[f'"{field}_{i}"'])) + for field in self.extra_event_fields_and_properties: + exprs.append(ast.Field(chain=[f'"{field}_{i}"'])) for exclusion_id, exclusion in enumerate(exclusions or []): if exclusion.funnelFromStep + 1 == i: @@ -205,8 +205,13 @@ def _get_comparison_cols(self, level_index: int, max_steps: int) -> List[ast.Exp ) ) - # for field in self.extra_event_fields_and_properties: - # exprs.append(f'if({comparison}, NULL, "{field}_{i}") as "{field}_{i}"') + for field in self.extra_event_fields_and_properties: + exprs.append( + parse_expr( + f'if({{comparison}}, NULL, "{field}_{i}") as "{field}_{i}"', + placeholders={"comparison": comparison}, + ) + ) for exclusion_id, exclusion in enumerate(exclusions or []): if exclusion.funnelFromStep + 1 == i: diff --git a/posthog/hogql_queries/insights/funnels/funnel_event_query.py b/posthog/hogql_queries/insights/funnels/funnel_event_query.py index 6078e1e7f57d3..ebf4dad02c555 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_event_query.py +++ b/posthog/hogql_queries/insights/funnels/funnel_event_query.py @@ -1,33 +1,48 @@ from typing import List, Set, Union +from posthog.clickhouse.materialized_columns.column import ColumnName from posthog.hogql import ast from posthog.hogql.parser import parse_expr from posthog.hogql_queries.insights.funnels.funnel_query_context import FunnelQueryContext from posthog.hogql_queries.insights.utils.properties import Properties from posthog.hogql_queries.utils.query_date_range import QueryDateRange from posthog.models.action.action import Action +from posthog.models.property.property import PropertyName from posthog.schema import ActionsNode, EventsNode, FunnelExclusionActionsNode, FunnelExclusionEventsNode from rest_framework.exceptions import ValidationError class FunnelEventQuery: context: FunnelQueryContext + _extra_fields: List[ColumnName] + _extra_event_properties: List[PropertyName] EVENT_TABLE_ALIAS = "e" def __init__( self, context: FunnelQueryContext, + extra_fields: List[ColumnName] = [], + extra_event_properties: List[PropertyName] = [], ): self.context = context + self._extra_fields = extra_fields + self._extra_event_properties = extra_event_properties + def to_query( self, # entities=None, # TODO: implement passed in entities when needed skip_entity_filter=False, ) -> ast.SelectQuery: + _extra_fields: List[ast.Expr] = [ + ast.Alias(alias=field, expr=ast.Field(chain=[self.EVENT_TABLE_ALIAS, field])) + for field in self._extra_fields + ] + select: List[ast.Expr] = [ ast.Alias(alias="timestamp", expr=ast.Field(chain=[self.EVENT_TABLE_ALIAS, "timestamp"])), ast.Alias(alias="aggregation_target", expr=self._aggregation_target_expr()), + *_extra_fields, ] select_from = ast.JoinExpr( diff --git a/posthog/hogql_queries/insights/funnels/funnel_strict.py b/posthog/hogql_queries/insights/funnels/funnel_strict.py index 503cf4de6e3bc..ec54fb67f6889 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_strict.py +++ b/posthog/hogql_queries/insights/funnels/funnel_strict.py @@ -109,8 +109,8 @@ def _get_partition_cols(self, level_index: int, max_steps: int): if i < level_index: exprs.append(ast.Field(chain=[f"latest_{i}"])) - # for field in self.extra_event_fields_and_properties: - # exprs.append(ast.Field(chain=[f'"{field}_{i}"'])) + for field in self.extra_event_fields_and_properties: + exprs.append(ast.Field(chain=[f'"{field}_{i}"'])) else: exprs.append( @@ -119,11 +119,11 @@ def _get_partition_cols(self, level_index: int, max_steps: int): ) ) - # for field in self.extra_event_fields_and_properties: - # exprs.append( - # parse_expr( - # f'min("{field}_{i}") over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN {i} PRECEDING AND {i} PRECEDING) "{field}_{i}"' - # ) - # ) + for field in self.extra_event_fields_and_properties: + exprs.append( + parse_expr( + f'min("{field}_{i}") over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN {i} PRECEDING AND {i} PRECEDING) "{field}_{i}"' + ) + ) return exprs diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py index 19897f122b187..8229cf56d07a8 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py @@ -1,5 +1,9 @@ -from datetime import datetime +from datetime import datetime, timedelta from typing import Dict, List, Optional, cast, Any +from uuid import UUID + +from django.utils import timezone +from freezegun import freeze_time from posthog.constants import INSIGHT_FUNNELS @@ -10,12 +14,14 @@ from posthog.models.person.util import bulk_create_persons from posthog.models.team.team import Team from posthog.schema import ActorsQuery, FunnelsActorsQuery, FunnelsQuery +from posthog.session_recordings.queries.test.session_replay_sql import produce_replay_summary from posthog.test.base import ( APIBaseTest, ClickhouseTestMixin, _create_event, _create_person, also_test_with_materialized_columns, + snapshot_clickhouse_queries, ) from posthog.test.test_journeys import journeys_for @@ -34,6 +40,7 @@ def get_actors( funnelTrendsDropOff: Optional[bool] = None, funnelTrendsEntrancePeriodStart: Optional[str] = None, offset: Optional[int] = None, + includeRecordings: bool = False, ): funnels_query = cast(FunnelsQuery, filter_to_query(filters)) funnel_actors_query = FunnelsActorsQuery( @@ -43,6 +50,7 @@ def get_actors( funnelStepBreakdown=funnelStepBreakdown, funnelTrendsDropOff=funnelTrendsDropOff, funnelTrendsEntrancePeriodStart=funnelTrendsEntrancePeriodStart, + includeRecordings=includeRecordings, ) actors_query = ActorsQuery(source=funnel_actors_query, offset=offset) response = ActorsQueryRunner(query=actors_query, team=team).calculate() @@ -486,113 +494,113 @@ def test_funnel_cohort_breakdown_persons(self): results = get_actors(filters, self.team, funnelStep=1) self.assertEqual(results[0][0]["id"], person.uuid) - # @snapshot_clickhouse_queries - # @freeze_time("2021-01-02 00:00:00.000Z") - # def test_funnel_person_recordings(self): - # p1 = _create_person(distinct_ids=[f"user_1"], team=self.team) - # _create_event( - # event="step one", - # distinct_id="user_1", - # team=self.team, - # timestamp=timezone.now().strftime("%Y-%m-%d %H:%M:%S.%f"), - # properties={"$session_id": "s1", "$window_id": "w1"}, - # event_uuid="11111111-1111-1111-1111-111111111111", - # ) - # _create_event( - # event="step two", - # distinct_id="user_1", - # team=self.team, - # timestamp=(timezone.now() + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S.%f"), - # properties={"$session_id": "s2", "$window_id": "w2"}, - # event_uuid="21111111-1111-1111-1111-111111111111", - # ) - # timestamp = datetime(2021, 1, 3, 0, 0, 0) - # produce_replay_summary( - # team_id=self.team.pk, - # session_id="s2", - # distinct_id="user_1", - # first_timestamp=timestamp, - # last_timestamp=timestamp, - # ) - - # # First event, but no recording - # filters = { - # "insight": INSIGHT_FUNNELS, - # "date_from": "2021-01-01", - # "date_to": "2021-01-08", - # "interval": "day", - # "funnel_window_days": 7, - # "events": [ - # {"id": "step one", "order": 0}, - # {"id": "step two", "order": 1}, - # {"id": "step three", "order": 2}, - # ], - # } - # # "include_recordings": "true", - # results = get_actors(filters, self.team, funnelStep=1) - # self.assertEqual(results[0]["id"], p1.uuid) - # self.assertEqual(results[0]["matched_recordings"], []) - - # # Second event, with recording - # filters = { - # "insight": INSIGHT_FUNNELS, - # "date_from": "2021-01-01", - # "date_to": "2021-01-08", - # "interval": "day", - # "funnel_window_days": 7, - # "events": [ - # {"id": "step one", "order": 0}, - # {"id": "step two", "order": 1}, - # {"id": "step three", "order": 2}, - # ], - # } - # # "include_recordings": "true", - # results = get_actors(filters, self.team, funnelStep=2) - # self.assertEqual(results[0]["id"], p1.uuid) - # self.assertEqual( - # results[0]["matched_recordings"], - # [ - # { - # "session_id": "s2", - # "events": [ - # { - # "uuid": UUID("21111111-1111-1111-1111-111111111111"), - # "timestamp": timezone.now() + timedelta(days=1), - # "window_id": "w2", - # } - # ], - # } - # ], - # ) - - # # Third event dropoff, with recording - # filters = { - # "insight": INSIGHT_FUNNELS, - # "date_from": "2021-01-01", - # "date_to": "2021-01-08", - # "interval": "day", - # "funnel_window_days": 7, - # "events": [ - # {"id": "step one", "order": 0}, - # {"id": "step two", "order": 1}, - # {"id": "step three", "order": 2}, - # ], - # } - # # "include_recordings": "true", - # results = get_actors(filters, self.team, funnelStep=-3) - # self.assertEqual(results[0]["id"], p1.uuid) - # self.assertEqual( - # results[0]["matched_recordings"], - # [ - # { - # "session_id": "s2", - # "events": [ - # { - # "uuid": UUID("21111111-1111-1111-1111-111111111111"), - # "timestamp": timezone.now() + timedelta(days=1), - # "window_id": "w2", - # } - # ], - # } - # ], - # ) + @snapshot_clickhouse_queries + @freeze_time("2021-01-02 00:00:00.000Z") + def test_funnel_person_recordings(self): + p1 = _create_person(distinct_ids=[f"user_1"], team=self.team) + _create_event( + event="step one", + distinct_id="user_1", + team=self.team, + timestamp=timezone.now().strftime("%Y-%m-%d %H:%M:%S.%f"), + properties={"$session_id": "s1", "$window_id": "w1"}, + event_uuid="11111111-1111-1111-1111-111111111111", + ) + _create_event( + event="step two", + distinct_id="user_1", + team=self.team, + timestamp=(timezone.now() + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S.%f"), + properties={"$session_id": "s2", "$window_id": "w2"}, + event_uuid="21111111-1111-1111-1111-111111111111", + ) + timestamp = datetime(2021, 1, 3, 0, 0, 0) + produce_replay_summary( + team_id=self.team.pk, + session_id="s2", + distinct_id="user_1", + first_timestamp=timestamp, + last_timestamp=timestamp, + ) + + # First event, but no recording + filters = { + "insight": INSIGHT_FUNNELS, + "date_from": "2021-01-01", + "date_to": "2021-01-08", + "interval": "day", + "funnel_window_days": 7, + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + {"id": "step three", "order": 2}, + ], + } + + results = get_actors(filters, self.team, funnelStep=1, includeRecordings=True) + self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual(results[0]["matched_recordings"], []) + + # Second event, with recording + filters = { + "insight": INSIGHT_FUNNELS, + "date_from": "2021-01-01", + "date_to": "2021-01-08", + "interval": "day", + "funnel_window_days": 7, + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + {"id": "step three", "order": 2}, + ], + } + + results = get_actors(filters, self.team, funnelStep=2, includeRecordings=True) + self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual( + results[0]["matched_recordings"], + [ + { + "session_id": "s2", + "events": [ + { + "uuid": UUID("21111111-1111-1111-1111-111111111111"), + "timestamp": timezone.now() + timedelta(days=1), + "window_id": "w2", + } + ], + } + ], + ) + + # Third event dropoff, with recording + filters = { + "insight": INSIGHT_FUNNELS, + "date_from": "2021-01-01", + "date_to": "2021-01-08", + "interval": "day", + "funnel_window_days": 7, + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + {"id": "step three", "order": 2}, + ], + } + + results = get_actors(filters, self.team, funnelStep=-3, includeRecordings=True) + self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual( + results[0]["matched_recordings"], + [ + { + "session_id": "s2", + "events": [ + { + "uuid": UUID("21111111-1111-1111-1111-111111111111"), + "timestamp": timezone.now() + timedelta(days=1), + "window_id": "w2", + } + ], + } + ], + ) From 89695eaea84e37db4fe51c02f4e8ff3826715499 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 20:45:31 +0000 Subject: [PATCH 03/17] Update query snapshots --- posthog/api/test/__snapshots__/test_query.ambr | 1 + 1 file changed, 1 insertion(+) diff --git a/posthog/api/test/__snapshots__/test_query.ambr b/posthog/api/test/__snapshots__/test_query.ambr index dc73cda0928a4..34651d435fb9b 100644 --- a/posthog/api/test/__snapshots__/test_query.ambr +++ b/posthog/api/test/__snapshots__/test_query.ambr @@ -166,6 +166,7 @@ events.elements_chain AS elements_chain, toTimeZone(events.created_at, 'UTC') AS created_at, events.`$session_id` AS `$session_id`, + events.`$window_id` AS `$window_id`, events.`$group_0` AS `$group_0`, events.`$group_1` AS `$group_1`, events.`$group_2` AS `$group_2`, From 03473b9056edb2440d8609a5053c1d4671489702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Mon, 26 Feb 2024 21:45:49 +0100 Subject: [PATCH 04/17] hogql string interpolation adjustments --- posthog/hogql_queries/insights/funnels/base.py | 4 ++-- posthog/hogql_queries/insights/funnels/funnel.py | 2 +- posthog/hogql_queries/insights/funnels/funnel_strict.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index fd94395dd999f..b95bf54f5b8eb 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -676,7 +676,7 @@ def _get_matching_events(self, max_steps: int) -> List[ast.Expr]: events = [] for i in range(0, max_steps): event_fields = ["latest"] + self.extra_event_fields_and_properties - event_fields_with_step = ", ".join([f'"{field}_{i}"' for field in event_fields]) + event_fields_with_step = ", ".join([f"{field}_{i}" for field in event_fields]) event_clause = f"({event_fields_with_step}) as step_{i}_matching_event" events.append(parse_expr(event_clause)) @@ -789,7 +789,7 @@ def _get_partition_cols(self, level_index: int, max_steps: int) -> List[ast.Expr exprs.append(ast.Field(chain=[f"latest_{i}"])) for field in self.extra_event_fields_and_properties: - exprs.append(ast.Field(chain=[f'"{field}_{i}"'])) + exprs.append(ast.Field(chain=[f"{field}_{i}"])) for exclusion_id, exclusion in enumerate(exclusions or []): if cast(int, exclusion.funnelFromStep) + 1 == i: diff --git a/posthog/hogql_queries/insights/funnels/funnel.py b/posthog/hogql_queries/insights/funnels/funnel.py index 7982dcc4b6ea0..e7425c6aed560 100644 --- a/posthog/hogql_queries/insights/funnels/funnel.py +++ b/posthog/hogql_queries/insights/funnels/funnel.py @@ -191,7 +191,7 @@ def _get_comparison_cols(self, level_index: int, max_steps: int) -> List[ast.Exp exprs.append(ast.Field(chain=[f"latest_{i}"])) for field in self.extra_event_fields_and_properties: - exprs.append(ast.Field(chain=[f'"{field}_{i}"'])) + exprs.append(ast.Field(chain=[f"{field}_{i}"])) for exclusion_id, exclusion in enumerate(exclusions or []): if exclusion.funnelFromStep + 1 == i: diff --git a/posthog/hogql_queries/insights/funnels/funnel_strict.py b/posthog/hogql_queries/insights/funnels/funnel_strict.py index ec54fb67f6889..7da26bfaf1390 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_strict.py +++ b/posthog/hogql_queries/insights/funnels/funnel_strict.py @@ -110,7 +110,7 @@ def _get_partition_cols(self, level_index: int, max_steps: int): exprs.append(ast.Field(chain=[f"latest_{i}"])) for field in self.extra_event_fields_and_properties: - exprs.append(ast.Field(chain=[f'"{field}_{i}"'])) + exprs.append(ast.Field(chain=[f"{field}_{i}"])) else: exprs.append( From 6ef14fab7157dc2058e26139bb68373e644501c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Mon, 26 Feb 2024 23:32:48 +0100 Subject: [PATCH 05/17] wip --- .../insights/funnels/funnel_trends_persons.py | 25 +- .../funnels/test/test_funnel_persons.py | 27 +- .../test/test_funnel_strict_persons.py | 286 +++++++++--------- .../test/test_funnel_trends_persons.py | 42 +-- .../test/test_funnel_unordered_persons.py | 107 ++++--- 5 files changed, 266 insertions(+), 221 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py b/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py index 571362e222957..9a387463efeb2 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py +++ b/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py @@ -40,18 +40,19 @@ def __init__(self, context: FunnelQueryContext, just_summarize=False): self.entrancePeriodStart = entrancePeriodStart def _get_funnel_person_step_events(self) -> List[ast.Expr]: - # if self._filter.include_recordings: - # # Get the event that should be used to match the recording - # funnel_to_step = self._filter.funnel_to_step - # is_drop_off = self._filter.drop_off - - # if funnel_to_step is None or is_drop_off: - # # If there is no funnel_to_step or if we are looking for drop off, we need to get the users final event - # return ", final_matching_events as matching_events" - # else: - # # Otherwise, we return the event of the funnel_to_step - # self.params.update({"matching_events_step_num": funnel_to_step}) - # return ", step_%(matching_events_step_num)s_matching_events as matching_events" + if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + # Get the event that should be used to match the recording + funnel_to_step = self.context.funnelsFilter.funnelToStep + is_drop_off = self.dropOff + + if funnel_to_step is None or is_drop_off: + # If there is no funnel_to_step or if we are looking for drop off, we need to get the users final event + return [ast.Alias(alias="matching_events", expr=ast.Field(chain=["final_matching_events"]))] + else: + # Otherwise, we return the event of the funnel_to_step + return [ + ast.Alias(alias="matching_events", expr=ast.Field(chain=[f"step_{funnel_to_step}_matching_events"])) + ] return [] def actor_query(self) -> ast.SelectQuery: diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py index 8229cf56d07a8..3e9a02cf18fc3 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py @@ -52,7 +52,11 @@ def get_actors( funnelTrendsEntrancePeriodStart=funnelTrendsEntrancePeriodStart, includeRecordings=includeRecordings, ) - actors_query = ActorsQuery(source=funnel_actors_query, offset=offset) + actors_query = ActorsQuery( + source=funnel_actors_query, + offset=offset, + select=["id", "person", *(["matched_recordings"] if includeRecordings else [])], + ) response = ActorsQueryRunner(query=actors_query, team=team).calculate() return response.results @@ -538,8 +542,13 @@ def test_funnel_person_recordings(self): } results = get_actors(filters, self.team, funnelStep=1, includeRecordings=True) - self.assertEqual(results[0]["id"], p1.uuid) - self.assertEqual(results[0]["matched_recordings"], []) + # self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual(results[0][0], p1.uuid) + self.assertEqual( + # results[0]["matched_recordings"], + list(results[0][2]), + [], + ) # Second event, with recording filters = { @@ -556,9 +565,11 @@ def test_funnel_person_recordings(self): } results = get_actors(filters, self.team, funnelStep=2, includeRecordings=True) - self.assertEqual(results[0]["id"], p1.uuid) + # self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual(results[0][0], p1.uuid) self.assertEqual( - results[0]["matched_recordings"], + # results[0]["matched_recordings"], + list(results[0][2]), [ { "session_id": "s2", @@ -588,9 +599,11 @@ def test_funnel_person_recordings(self): } results = get_actors(filters, self.team, funnelStep=-3, includeRecordings=True) - self.assertEqual(results[0]["id"], p1.uuid) + # self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual(results[0][0], p1.uuid) self.assertEqual( - results[0]["matched_recordings"], + # results[0]["matched_recordings"], + list(results[0][2]), [ { "session_id": "s2", diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_strict_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_strict_persons.py index 53ea15ed5e0ee..182442e02c3ee 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_strict_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_strict_persons.py @@ -1,11 +1,19 @@ -from datetime import datetime +from datetime import datetime, timedelta +from uuid import UUID + +from django.utils import timezone +from freezegun import freeze_time from posthog.constants import INSIGHT_FUNNELS from posthog.hogql_queries.insights.funnels.test.test_funnel_persons import get_actors +from posthog.session_recordings.queries.test.session_replay_sql import produce_replay_summary from posthog.test.base import ( APIBaseTest, ClickhouseTestMixin, + _create_event, + _create_person, + snapshot_clickhouse_queries, ) from posthog.test.test_journeys import journeys_for @@ -115,138 +123,144 @@ def test_third_step(self): self.assertEqual(0, len(results)) - # @snapshot_clickhouse_queries - # @freeze_time("2021-01-02 00:00:00.000Z") - # def test_strict_funnel_person_recordings(self): - # p1 = _create_person(distinct_ids=[f"user_1"], team=self.team) - # _create_event( - # event="step one", - # distinct_id="user_1", - # team=self.team, - # timestamp=timezone.now().strftime("%Y-%m-%d %H:%M:%S.%f"), - # properties={"$session_id": "s1", "$window_id": "w1"}, - # event_uuid="11111111-1111-1111-1111-111111111111", - # ) - # _create_event( - # event="step two", - # distinct_id="user_1", - # team=self.team, - # timestamp=(timezone.now() + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S.%f"), - # properties={"$session_id": "s2", "$window_id": "w2"}, - # event_uuid="21111111-1111-1111-1111-111111111111", - # ) - # _create_event( - # event="interupting step", - # distinct_id="user_1", - # team=self.team, - # timestamp=(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d %H:%M:%S.%f"), - # properties={"$session_id": "s2", "$window_id": "w2"}, - # event_uuid="21111111-1111-1111-1111-111111111111", - # ) - # _create_event( - # event="step three", - # distinct_id="user_1", - # team=self.team, - # timestamp=(timezone.now() + timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S.%f"), - # properties={"$session_id": "s2", "$window_id": "w2"}, - # event_uuid="21111111-1111-1111-1111-111111111111", - # ) - # timestamp = datetime(2021, 1, 3, 0, 0, 0) - # produce_replay_summary( - # team_id=self.team.pk, - # session_id="s2", - # distinct_id="user_1", - # first_timestamp=timestamp, - # last_timestamp=timestamp, - # ) - - # # First event, but no recording - # filters = { - # "insight": INSIGHT_FUNNELS, - # "funnel_order_type": "strict", - # "date_from": "2021-01-01", - # "date_to": "2021-01-08", - # "interval": "day", - # "funnel_window_days": 7, - # "events": [ - # {"id": "step one", "order": 0}, - # {"id": "step two", "order": 1}, - # {"id": "step three", "order": 2}, - # ], - # } - - # # "include_recordings": "true", - # results = get_actors(filters, self.team, funnelStep=1) - - # self.assertEqual(results[0]["id"], p1.uuid) - # self.assertEqual(results[0]["matched_recordings"], []) - - # # Second event, with recording - # filters = { - # "insight": INSIGHT_FUNNELS, - # "funnel_order_type": "strict", - # "date_from": "2021-01-01", - # "date_to": "2021-01-08", - # "interval": "day", - # "funnel_window_days": 7, - # "events": [ - # {"id": "step one", "order": 0}, - # {"id": "step two", "order": 1}, - # {"id": "step three", "order": 2}, - # ], - # } - - # # "include_recordings": "true", - # results = get_actors(filters, self.team, funnelStep=2) - - # self.assertEqual(results[0]["id"], p1.uuid) - # self.assertEqual( - # results[0]["matched_recordings"], - # [ - # { - # "session_id": "s2", - # "events": [ - # { - # "uuid": UUID("21111111-1111-1111-1111-111111111111"), - # "timestamp": timezone.now() + timedelta(days=1), - # "window_id": "w2", - # } - # ], - # } - # ], - # ) - - # # Third event dropoff, with recording - # filters = { - # "insight": INSIGHT_FUNNELS, - # "funnel_order_type": "strict", - # "date_from": "2021-01-01", - # "date_to": "2021-01-08", - # "interval": "day", - # "funnel_window_days": 7, - # "events": [ - # {"id": "step one", "order": 0}, - # {"id": "step two", "order": 1}, - # {"id": "step three", "order": 2}, - # ], - # } - - # # "include_recordings": "true", - # results = get_actors(filters, self.team, funnelStep=-3) - - # self.assertEqual(results[0]["id"], p1.uuid) - # self.assertEqual( - # results[0]["matched_recordings"], - # [ - # { - # "session_id": "s2", - # "events": [ - # { - # "uuid": UUID("21111111-1111-1111-1111-111111111111"), - # "timestamp": timezone.now() + timedelta(days=1), - # "window_id": "w2", - # } - # ], - # } - # ], - # ) + @snapshot_clickhouse_queries + @freeze_time("2021-01-02 00:00:00.000Z") + def test_strict_funnel_person_recordings(self): + p1 = _create_person(distinct_ids=[f"user_1"], team=self.team) + _create_event( + event="step one", + distinct_id="user_1", + team=self.team, + timestamp=timezone.now().strftime("%Y-%m-%d %H:%M:%S.%f"), + properties={"$session_id": "s1", "$window_id": "w1"}, + event_uuid="11111111-1111-1111-1111-111111111111", + ) + _create_event( + event="step two", + distinct_id="user_1", + team=self.team, + timestamp=(timezone.now() + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S.%f"), + properties={"$session_id": "s2", "$window_id": "w2"}, + event_uuid="21111111-1111-1111-1111-111111111111", + ) + _create_event( + event="interupting step", + distinct_id="user_1", + team=self.team, + timestamp=(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d %H:%M:%S.%f"), + properties={"$session_id": "s2", "$window_id": "w2"}, + event_uuid="21111111-1111-1111-1111-111111111111", + ) + _create_event( + event="step three", + distinct_id="user_1", + team=self.team, + timestamp=(timezone.now() + timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S.%f"), + properties={"$session_id": "s2", "$window_id": "w2"}, + event_uuid="21111111-1111-1111-1111-111111111111", + ) + timestamp = datetime(2021, 1, 3, 0, 0, 0) + produce_replay_summary( + team_id=self.team.pk, + session_id="s2", + distinct_id="user_1", + first_timestamp=timestamp, + last_timestamp=timestamp, + ) + + # First event, but no recording + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "strict", + "date_from": "2021-01-01", + "date_to": "2021-01-08", + "interval": "day", + "funnel_window_days": 7, + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + {"id": "step three", "order": 2}, + ], + } + + results = get_actors(filters, self.team, funnelStep=1, includeRecordings=True) + + # self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual(results[0][0], p1.uuid) + self.assertEqual( + # results[0]["matched_recordings"], + list(results[0][2]), + [], + ) + + # Second event, with recording + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "strict", + "date_from": "2021-01-01", + "date_to": "2021-01-08", + "interval": "day", + "funnel_window_days": 7, + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + {"id": "step three", "order": 2}, + ], + } + + results = get_actors(filters, self.team, funnelStep=2, includeRecordings=True) + + # self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual(results[0][0], p1.uuid) + self.assertEqual( + # results[0]["matched_recordings"], + list(results[0][2]), + [ + { + "session_id": "s2", + "events": [ + { + "uuid": UUID("21111111-1111-1111-1111-111111111111"), + "timestamp": timezone.now() + timedelta(days=1), + "window_id": "w2", + } + ], + } + ], + ) + + # Third event dropoff, with recording + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "strict", + "date_from": "2021-01-01", + "date_to": "2021-01-08", + "interval": "day", + "funnel_window_days": 7, + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + {"id": "step three", "order": 2}, + ], + } + + results = get_actors(filters, self.team, funnelStep=-3, includeRecordings=True) + + # self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual(results[0][0], p1.uuid) + self.assertEqual( + # results[0]["matched_recordings"], + list(results[0][2]), + [ + { + "session_id": "s2", + "events": [ + { + "uuid": UUID("21111111-1111-1111-1111-111111111111"), + "timestamp": timezone.now() + timedelta(days=1), + "window_id": "w2", + } + ], + } + ], + ) diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py index e1f4125c9e0ae..b37f57f368489 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py @@ -62,19 +62,21 @@ def test_funnel_trend_persons_returns_recordings(self): last_timestamp=timestamp, ) - # "include_recordings": "true", results = get_actors( {"funnel_to_step": 1, **filters}, self.team, funnelTrendsDropOff=False, funnelTrendsEntrancePeriodStart="2021-05-01 00:00:00", + includeRecordings=True, ) - self.assertEqual([person[0]["id"] for person in results], [persons["user_one"].uuid]) - # self.assertEqual( - # [person["matched_recordings"][0]["session_id"] for person in results], - # ["s1b"], - # ) + # self.assertEqual([person[0]["id"] for person in results], [persons["user_one"].uuid]) + self.assertEqual([person[0][0] for person in results], [persons["user_one"].uuid]) + self.assertEqual( + # [person["matched_recordings"][0]["session_id"] for person in results], + [list(person[0][2])["session_id"] for person in results], + ["s1b"], + ) @snapshot_clickhouse_queries def test_funnel_trend_persons_with_no_to_step(self): @@ -110,19 +112,21 @@ def test_funnel_trend_persons_with_no_to_step(self): last_timestamp=timestamp, ) - # "include_recordings": "true", results = get_actors( filters, self.team, funnelTrendsDropOff=False, funnelTrendsEntrancePeriodStart="2021-05-01 00:00:00", + includeRecordings=True, ) - self.assertEqual([person[0]["id"] for person in results], [persons["user_one"].uuid]) - # self.assertEqual( - # [person["matched_recordings"][0]["session_id"] for person in results], - # ["s1c"], - # ) + # self.assertEqual([person[0]["id"] for person in results], [persons["user_one"].uuid]) + self.assertEqual([person[0][0] for person in results], [persons["user_one"].uuid]) + self.assertEqual( + # [person["matched_recordings"][0]["session_id"] for person in results], + [list(person[0][2])["session_id"] for person in results], + ["s1c"], + ) @snapshot_clickhouse_queries def test_funnel_trend_persons_with_drop_off(self): @@ -147,16 +151,18 @@ def test_funnel_trend_persons_with_drop_off(self): last_timestamp=timestamp, ) - # "include_recordings": "true", results = get_actors( filters, self.team, funnelTrendsDropOff=True, funnelTrendsEntrancePeriodStart="2021-05-01 00:00:00", + includeRecordings=True, ) - self.assertEqual([person[0]["id"] for person in results], [persons["user_one"].uuid]) - # self.assertEqual( - # [person["matched_recordings"][0].get("session_id") for person in results], - # ["s1a"], - # ) + # self.assertEqual([person[0]["id"] for person in results], [persons["user_one"].uuid]) + self.assertEqual([person[0][0] for person in results], [persons["user_one"].uuid]) + self.assertEqual( + # [person["matched_recordings"][0].get("session_id") for person in results], + [list(person[0][2])["session_id"] for person in results], + ["s1a"], + ) diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered_persons.py index 0d9861c1ff75c..90377aa728508 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered_persons.py @@ -1,11 +1,17 @@ -from datetime import datetime +from datetime import datetime, timedelta +from freezegun import freeze_time +from django.utils import timezone from posthog.constants import INSIGHT_FUNNELS from posthog.hogql_queries.insights.funnels.test.test_funnel_persons import get_actors +from posthog.session_recordings.queries.test.session_replay_sql import produce_replay_summary from posthog.test.base import ( APIBaseTest, ClickhouseTestMixin, + _create_event, + _create_person, + snapshot_clickhouse_queries, ) from posthog.test.test_journeys import journeys_for @@ -134,52 +140,57 @@ def test_last_step_dropoff(self): self.assertEqual(10, len(results)) - # @snapshot_clickhouse_queries - # @freeze_time("2021-01-02 00:00:00.000Z") - # def test_unordered_funnel_does_not_return_recordings(self): - # p1 = _create_person(distinct_ids=[f"user_1"], team=self.team) - # _create_event( - # event="step two", - # distinct_id="user_1", - # team=self.team, - # timestamp=timezone.now().strftime("%Y-%m-%d %H:%M:%S.%f"), - # properties={"$session_id": "s1", "$window_id": "w1"}, - # event_uuid="21111111-1111-1111-1111-111111111111", - # ) - # _create_event( - # event="step one", - # distinct_id="user_1", - # team=self.team, - # timestamp=(timezone.now() + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S.%f"), - # properties={"$session_id": "s1", "$window_id": "w1"}, - # event_uuid="11111111-1111-1111-1111-111111111111", - # ) - - # timestamp = timezone.now() + timedelta(days=1) - # produce_replay_summary( - # team_id=self.team.pk, - # session_id="s1", - # distinct_id="user_1", - # first_timestamp=timestamp, - # last_timestamp=timestamp, - # ) + @snapshot_clickhouse_queries + @freeze_time("2021-01-02 00:00:00.000Z") + def test_unordered_funnel_does_not_return_recordings(self): + p1 = _create_person(distinct_ids=[f"user_1"], team=self.team) + _create_event( + event="step two", + distinct_id="user_1", + team=self.team, + timestamp=timezone.now().strftime("%Y-%m-%d %H:%M:%S.%f"), + properties={"$session_id": "s1", "$window_id": "w1"}, + event_uuid="21111111-1111-1111-1111-111111111111", + ) + _create_event( + event="step one", + distinct_id="user_1", + team=self.team, + timestamp=(timezone.now() + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S.%f"), + properties={"$session_id": "s1", "$window_id": "w1"}, + event_uuid="11111111-1111-1111-1111-111111111111", + ) + + timestamp = timezone.now() + timedelta(days=1) + produce_replay_summary( + team_id=self.team.pk, + session_id="s1", + distinct_id="user_1", + first_timestamp=timestamp, + last_timestamp=timestamp, + ) - # filters = { - # "insight": INSIGHT_FUNNELS, - # "funnel_order_type": "unordered", - # "date_from": "2021-01-01", - # "date_to": "2021-01-08", - # "interval": "day", - # "funnel_window_days": 7, - # "funnel_step": 1, - # "events": [ - # {"id": "step one", "order": 0}, - # {"id": "step two", "order": 1}, - # {"id": "step three", "order": 2}, - # ], - # } - # # "include_recordings": "true", # <- The important line - # results = get_actors(filters, self.team, funnelStep=1) + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "date_from": "2021-01-01", + "date_to": "2021-01-08", + "interval": "day", + "funnel_window_days": 7, + "funnel_step": 1, + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + {"id": "step three", "order": 2}, + ], + } + + results = get_actors(filters, self.team, funnelStep=1, includeRecordings=True) - # self.assertEqual(results[0]["id"], p1.uuid) - # self.assertEqual(results[0]["matched_recordings"], []) + # self.assertEqual(results[0]["id"], p1.uuid) + self.assertEqual(results[0][0], p1.uuid) + self.assertEqual( + # results[0]["matched_recordings"], + list(results[0][2]), + [], + ) From 891a235b63c8b529842e13671419d00578d6d4b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Mon, 26 Feb 2024 23:35:05 +0100 Subject: [PATCH 06/17] wip --- .../hogql_queries/insights/funnels/funnel_unordered_persons.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py b/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py index 08e4b210b4f67..bb45b0014d81f 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py +++ b/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py @@ -9,8 +9,7 @@ class FunnelUnorderedActors(FunnelUnordered): def _get_funnel_person_step_events(self) -> List[ast.Expr]: # Unordered funnels does not support matching events (and thereby recordings), # but it simplifies the logic if we return an empty array for matching events - # if self._filter.include_recordings: - if False: + if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: return [parse_expr("array() as matching_events")] # type: ignore return [] From d9d8c614b0a7544bcca3142845b37556cba0e0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Mon, 26 Feb 2024 23:50:06 +0100 Subject: [PATCH 07/17] types --- posthog/hogql_queries/insights/funnels/base.py | 4 ++-- .../insights/funnels/funnel_unordered_persons.py | 2 +- .../insights/funnels/test/test_funnel_trends_persons.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index b95bf54f5b8eb..269738a13448e 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -394,7 +394,7 @@ def _get_inner_event_query( ) entities_to_use = entities or query.series - extra_fields = [] + extra_fields: List[str] = [] # for prop in self._include_properties: # extra_fields.append(prop) @@ -632,7 +632,7 @@ def _get_funnel_person_step_events(self) -> List[ast.Expr]: # if self._filter.include_final_matching_events: if False: # TODO: Implement with correlations # Always returns the user's final step of the funnel - return [parse_expr("final_matching_events as matching_events")] + return [parse_expr("final_matching_events as matching_events")] # type: ignore elif step_num is None: raise ValueError("Missing funnelStep actors query property") if step_num >= 0: diff --git a/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py b/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py index bb45b0014d81f..b3d1cb4dc1da4 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py +++ b/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py @@ -10,7 +10,7 @@ def _get_funnel_person_step_events(self) -> List[ast.Expr]: # Unordered funnels does not support matching events (and thereby recordings), # but it simplifies the logic if we return an empty array for matching events if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: - return [parse_expr("array() as matching_events")] # type: ignore + return [parse_expr("array() as matching_events")] return [] def actor_query( diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py index b37f57f368489..347762471cdd9 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py @@ -74,7 +74,7 @@ def test_funnel_trend_persons_returns_recordings(self): self.assertEqual([person[0][0] for person in results], [persons["user_one"].uuid]) self.assertEqual( # [person["matched_recordings"][0]["session_id"] for person in results], - [list(person[0][2])["session_id"] for person in results], + [list(person[0][2])["session_id"] for person in results], # type: ignore ["s1b"], ) @@ -124,7 +124,7 @@ def test_funnel_trend_persons_with_no_to_step(self): self.assertEqual([person[0][0] for person in results], [persons["user_one"].uuid]) self.assertEqual( # [person["matched_recordings"][0]["session_id"] for person in results], - [list(person[0][2])["session_id"] for person in results], + [list(person[0][2])["session_id"] for person in results], # type: ignore ["s1c"], ) @@ -163,6 +163,6 @@ def test_funnel_trend_persons_with_drop_off(self): self.assertEqual([person[0][0] for person in results], [persons["user_one"].uuid]) self.assertEqual( # [person["matched_recordings"][0].get("session_id") for person in results], - [list(person[0][2])["session_id"] for person in results], + [list(person[0][2])["session_id"] for person in results], # type: ignore ["s1a"], ) From d1c32e9dd8a14e05931302110280a63f18580436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Tue, 27 Feb 2024 00:03:54 +0100 Subject: [PATCH 08/17] fixes --- .../hogql_queries/insights/funnels/base.py | 24 +++++++++++++++---- .../insights/funnels/funnel_trends.py | 6 ++++- .../insights/funnels/funnel_trends_persons.py | 6 ++++- .../funnels/funnel_unordered_persons.py | 6 ++++- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index 269738a13448e..6adf006646d6f 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -45,7 +45,11 @@ def __init__(self, context: FunnelQueryContext): self._extra_event_fields: List[ColumnName] = [] self._extra_event_properties: List[PropertyName] = [] - if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + if ( + hasattr(self.context, "ActorsQuery") + and self.context.actorsQuery is not None + and self.context.actorsQuery.includeRecordings + ): self._extra_event_fields = ["uuid"] self._extra_event_properties = ["$session_id", "$window_id"] @@ -627,7 +631,11 @@ def _get_funnel_person_step_condition(self) -> ast.Expr: return ast.And(exprs=conditions) def _get_funnel_person_step_events(self) -> List[ast.Expr]: - if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + if ( + hasattr(self.context, "ActorsQuery") + and self.context.actorsQuery is not None + and self.context.actorsQuery.includeRecordings + ): step_num = self.context.actorsQuery.funnelStep # if self._filter.include_final_matching_events: if False: # TODO: Implement with correlations @@ -672,7 +680,11 @@ def _get_final_matching_event(self, max_steps: int) -> List[ast.Expr]: return [parse_expr(f"{statement} as final_matching_event")] if statement else [] def _get_matching_events(self, max_steps: int) -> List[ast.Expr]: - if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + if ( + hasattr(self.context, "ActorsQuery") + and self.context.actorsQuery is not None + and self.context.actorsQuery.includeRecordings + ): events = [] for i in range(0, max_steps): event_fields = ["latest"] + self.extra_event_fields_and_properties @@ -685,7 +697,11 @@ def _get_matching_events(self, max_steps: int) -> List[ast.Expr]: def _get_matching_event_arrays(self, max_steps: int) -> List[ast.Expr]: exprs: List[ast.Expr] = [] - if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + if ( + hasattr(self.context, "ActorsQuery") + and self.context.actorsQuery is not None + and self.context.actorsQuery.includeRecordings + ): for i in range(0, max_steps): exprs.append(parse_expr(f"groupArray(10)(step_{i}_matching_event) as step_{i}_matching_events")) exprs.append(parse_expr(f"groupArray(10)(final_matching_event) as final_matching_events")) diff --git a/posthog/hogql_queries/insights/funnels/funnel_trends.py b/posthog/hogql_queries/insights/funnels/funnel_trends.py index 9b552ad298ac9..25c598ea796d1 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_trends.py +++ b/posthog/hogql_queries/insights/funnels/funnel_trends.py @@ -273,7 +273,11 @@ def get_step_counts_without_aggregation_query( steps_per_person_query = self.funnel_order.get_step_counts_without_aggregation_query() event_select_clause: List[ast.Expr] = [] - if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + if ( + hasattr(self.context, "ActorsQuery") + and self.context.actorsQuery is not None + and self.context.actorsQuery.includeRecordings + ): event_select_clause = self._get_matching_event_arrays(max_steps) breakdown_clause = self._get_breakdown_prop_expr() diff --git a/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py b/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py index 9a387463efeb2..cbbe6d7f45828 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py +++ b/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py @@ -40,7 +40,11 @@ def __init__(self, context: FunnelQueryContext, just_summarize=False): self.entrancePeriodStart = entrancePeriodStart def _get_funnel_person_step_events(self) -> List[ast.Expr]: - if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + if ( + hasattr(self.context, "ActorsQuery") + and self.context.actorsQuery is not None + and self.context.actorsQuery.includeRecordings + ): # Get the event that should be used to match the recording funnel_to_step = self.context.funnelsFilter.funnelToStep is_drop_off = self.dropOff diff --git a/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py b/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py index b3d1cb4dc1da4..e1c8d486b507c 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py +++ b/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py @@ -9,7 +9,11 @@ class FunnelUnorderedActors(FunnelUnordered): def _get_funnel_person_step_events(self) -> List[ast.Expr]: # Unordered funnels does not support matching events (and thereby recordings), # but it simplifies the logic if we return an empty array for matching events - if self.context.actorsQuery and self.context.actorsQuery.includeRecordings: + if ( + hasattr(self.context, "ActorsQuery") + and self.context.actorsQuery is not None + and self.context.actorsQuery.includeRecordings + ): return [parse_expr("array() as matching_events")] return [] From 0b2c31683d5f9c50a41a926dd752d8a393d7c7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Tue, 27 Feb 2024 01:14:51 +0100 Subject: [PATCH 09/17] fix typo --- posthog/hogql_queries/insights/funnels/base.py | 8 ++++---- posthog/hogql_queries/insights/funnels/funnel_trends.py | 2 +- .../insights/funnels/funnel_trends_persons.py | 2 +- .../insights/funnels/funnel_unordered_persons.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index 6adf006646d6f..13f7ead0c006f 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -46,7 +46,7 @@ def __init__(self, context: FunnelQueryContext): self._extra_event_properties: List[PropertyName] = [] if ( - hasattr(self.context, "ActorsQuery") + hasattr(self.context, "actorsQuery") and self.context.actorsQuery is not None and self.context.actorsQuery.includeRecordings ): @@ -632,7 +632,7 @@ def _get_funnel_person_step_condition(self) -> ast.Expr: def _get_funnel_person_step_events(self) -> List[ast.Expr]: if ( - hasattr(self.context, "ActorsQuery") + hasattr(self.context, "actorsQuery") and self.context.actorsQuery is not None and self.context.actorsQuery.includeRecordings ): @@ -681,7 +681,7 @@ def _get_final_matching_event(self, max_steps: int) -> List[ast.Expr]: def _get_matching_events(self, max_steps: int) -> List[ast.Expr]: if ( - hasattr(self.context, "ActorsQuery") + hasattr(self.context, "actorsQuery") and self.context.actorsQuery is not None and self.context.actorsQuery.includeRecordings ): @@ -698,7 +698,7 @@ def _get_matching_events(self, max_steps: int) -> List[ast.Expr]: def _get_matching_event_arrays(self, max_steps: int) -> List[ast.Expr]: exprs: List[ast.Expr] = [] if ( - hasattr(self.context, "ActorsQuery") + hasattr(self.context, "actorsQuery") and self.context.actorsQuery is not None and self.context.actorsQuery.includeRecordings ): diff --git a/posthog/hogql_queries/insights/funnels/funnel_trends.py b/posthog/hogql_queries/insights/funnels/funnel_trends.py index 25c598ea796d1..5c370512a20e8 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_trends.py +++ b/posthog/hogql_queries/insights/funnels/funnel_trends.py @@ -274,7 +274,7 @@ def get_step_counts_without_aggregation_query( event_select_clause: List[ast.Expr] = [] if ( - hasattr(self.context, "ActorsQuery") + hasattr(self.context, "actorsQuery") and self.context.actorsQuery is not None and self.context.actorsQuery.includeRecordings ): diff --git a/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py b/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py index cbbe6d7f45828..c90a9ed576270 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py +++ b/posthog/hogql_queries/insights/funnels/funnel_trends_persons.py @@ -41,7 +41,7 @@ def __init__(self, context: FunnelQueryContext, just_summarize=False): def _get_funnel_person_step_events(self) -> List[ast.Expr]: if ( - hasattr(self.context, "ActorsQuery") + hasattr(self.context, "actorsQuery") and self.context.actorsQuery is not None and self.context.actorsQuery.includeRecordings ): diff --git a/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py b/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py index e1c8d486b507c..2af375ab1f23d 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py +++ b/posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py @@ -10,7 +10,7 @@ def _get_funnel_person_step_events(self) -> List[ast.Expr]: # Unordered funnels does not support matching events (and thereby recordings), # but it simplifies the logic if we return an empty array for matching events if ( - hasattr(self.context, "ActorsQuery") + hasattr(self.context, "actorsQuery") and self.context.actorsQuery is not None and self.context.actorsQuery.includeRecordings ): From 8d2e535198f1c25421f37f5a92a93413eed09793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Tue, 27 Feb 2024 02:00:30 +0100 Subject: [PATCH 10/17] fixes --- .../funnels/test/test_funnel_trends_persons.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py index 347762471cdd9..5a994ccdd746c 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py @@ -71,10 +71,10 @@ def test_funnel_trend_persons_returns_recordings(self): ) # self.assertEqual([person[0]["id"] for person in results], [persons["user_one"].uuid]) - self.assertEqual([person[0][0] for person in results], [persons["user_one"].uuid]) + self.assertEqual(results[0][0], persons["user_one"].uuid) self.assertEqual( # [person["matched_recordings"][0]["session_id"] for person in results], - [list(person[0][2])["session_id"] for person in results], # type: ignore + [list(results[0][2])[0]["session_id"]], # type: ignore ["s1b"], ) @@ -121,10 +121,10 @@ def test_funnel_trend_persons_with_no_to_step(self): ) # self.assertEqual([person[0]["id"] for person in results], [persons["user_one"].uuid]) - self.assertEqual([person[0][0] for person in results], [persons["user_one"].uuid]) + self.assertEqual(results[0][0], persons["user_one"].uuid) self.assertEqual( # [person["matched_recordings"][0]["session_id"] for person in results], - [list(person[0][2])["session_id"] for person in results], # type: ignore + [list(results[0][2])[0]["session_id"]], # type: ignore ["s1c"], ) @@ -160,9 +160,9 @@ def test_funnel_trend_persons_with_drop_off(self): ) # self.assertEqual([person[0]["id"] for person in results], [persons["user_one"].uuid]) - self.assertEqual([person[0][0] for person in results], [persons["user_one"].uuid]) + self.assertEqual(results[0][0], persons["user_one"].uuid) self.assertEqual( # [person["matched_recordings"][0].get("session_id") for person in results], - [list(person[0][2])["session_id"] for person in results], # type: ignore + [list(results[0][2])[0]["session_id"]], # type: ignore ["s1a"], ) From c0dc855bb3eec13123a18595db1ee9580a945d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Tue, 27 Feb 2024 10:19:08 +0100 Subject: [PATCH 11/17] frontend --- frontend/src/scenes/funnels/funnelPersonsModalLogic.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts b/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts index 909ace73eb886..dc4d70890b4aa 100644 --- a/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts +++ b/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts @@ -102,8 +102,9 @@ export const funnelPersonsModalLogic = kea([ kind: NodeKind.InsightActorsQuery, source: values.querySource!, funnelStep: converted ? stepNo : -stepNo, + includeRecordings: true, } - openPersonsModal({ title, query }) + openPersonsModal({ title, query, additionalSelect: { matched_recordings: 'matched_recordings' } }) } else { openPersonsModal({ url: generateBaselineConversionUrl(converted ? step.converted_people_url : step.dropped_people_url), @@ -134,8 +135,9 @@ export const funnelPersonsModalLogic = kea([ source: values.querySource!, funnelStep: converted ? stepNo : -stepNo, funnelStepBreakdown: series.breakdown_value, + includeRecordings: true, } - openPersonsModal({ title, query }) + openPersonsModal({ title, query, additionalSelect: { matched_recordings: 'matched_recordings' } }) } else { openPersonsModal({ url: converted ? series.converted_people_url : series.dropped_people_url, From 4eaba71d43cb5c07b969863ec2410a54514f9932 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:39:51 +0000 Subject: [PATCH 12/17] Update query snapshots --- .../test_funnel_strict_persons.ambr | 427 ++++++++++++++++++ .../test_funnel_trends_persons.ambr | 312 +++++++++++-- .../test_funnel_unordered_persons.ambr | 287 ++++++++++++ 3 files changed, 984 insertions(+), 42 deletions(-) create mode 100644 posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons.ambr create mode 100644 posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered_persons.ambr diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons.ambr new file mode 100644 index 0000000000000..0b2f21460022e --- /dev/null +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons.ambr @@ -0,0 +1,427 @@ +# serializer version: 1 +# name: TestFunnelStrictStepsPersons.test_strict_funnel_person_recordings + ''' + SELECT persons.id, + persons.id AS id, + source.matching_events AS matching_events + FROM + (SELECT person.id AS id + FROM person + WHERE equals(person.team_id, 2) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons + INNER JOIN + (SELECT aggregation_target AS actor_id, + step_0_matching_events AS matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + avg(step_1_conversion_time) AS step_1_average_conversion_time_inner, + avg(step_2_conversion_time) AS step_2_average_conversion_time_inner, + median(step_1_conversion_time) AS step_1_median_conversion_time_inner, + median(step_2_conversion_time) AS step_2_median_conversion_time_inner, + groupArray(10)(step_0_matching_event) AS step_0_matching_events, + groupArray(10)(step_1_matching_event) AS step_1_matching_events, + groupArray(10)(step_2_matching_event) AS step_2_matching_events, + groupArray(10)(final_matching_event) AS final_matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, + step_1_conversion_time AS step_1_conversion_time, + step_2_conversion_time AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + min(latest_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, + min(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS uuid_1, + min(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS `$session_id_1`, + min(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS latest_2, + min(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS uuid_2, + min(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS `$session_id_2`, + min(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS `$window_id_2` + FROM + (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, + e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, + if(equals(e.event, 'step one'), 1, 0) AS step_0, + if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, + if(equals(e.event, 'step two'), 1, 0) AS step_1, + if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, + if(equals(e.event, 'step three'), 1, 0) AS step_2, + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` + FROM events AS e + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-08 23:59:59.999999', 6, 'UTC')))))) + WHERE ifNull(equals(step_0, 1), 0))) + GROUP BY aggregation_target, + steps + HAVING ifNull(equals(steps, max_steps), isNull(steps) + and isNull(max_steps))) + WHERE ifNull(in(steps, [1, 2, 3]), 0) + ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) + ORDER BY persons.id ASC + LIMIT 101 + OFFSET 0 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelStrictStepsPersons.test_strict_funnel_person_recordings.1 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, ['s1']), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelStrictStepsPersons.test_strict_funnel_person_recordings.2 + ''' + SELECT persons.id, + persons.id AS id, + source.matching_events AS matching_events + FROM + (SELECT person.id AS id + FROM person + WHERE equals(person.team_id, 2) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons + INNER JOIN + (SELECT aggregation_target AS actor_id, + step_1_matching_events AS matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + avg(step_1_conversion_time) AS step_1_average_conversion_time_inner, + avg(step_2_conversion_time) AS step_2_average_conversion_time_inner, + median(step_1_conversion_time) AS step_1_median_conversion_time_inner, + median(step_2_conversion_time) AS step_2_median_conversion_time_inner, + groupArray(10)(step_0_matching_event) AS step_0_matching_events, + groupArray(10)(step_1_matching_event) AS step_1_matching_events, + groupArray(10)(step_2_matching_event) AS step_2_matching_events, + groupArray(10)(final_matching_event) AS final_matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, + step_1_conversion_time AS step_1_conversion_time, + step_2_conversion_time AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + min(latest_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, + min(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS uuid_1, + min(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS `$session_id_1`, + min(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS latest_2, + min(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS uuid_2, + min(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS `$session_id_2`, + min(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS `$window_id_2` + FROM + (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, + e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, + if(equals(e.event, 'step one'), 1, 0) AS step_0, + if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, + if(equals(e.event, 'step two'), 1, 0) AS step_1, + if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, + if(equals(e.event, 'step three'), 1, 0) AS step_2, + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` + FROM events AS e + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-08 23:59:59.999999', 6, 'UTC')))))) + WHERE ifNull(equals(step_0, 1), 0))) + GROUP BY aggregation_target, + steps + HAVING ifNull(equals(steps, max_steps), isNull(steps) + and isNull(max_steps))) + WHERE ifNull(in(steps, [2, 3]), 0) + ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) + ORDER BY persons.id ASC + LIMIT 101 + OFFSET 0 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelStrictStepsPersons.test_strict_funnel_person_recordings.3 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, ['s2']), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelStrictStepsPersons.test_strict_funnel_person_recordings.4 + ''' + SELECT persons.id, + persons.id AS id, + source.matching_events AS matching_events + FROM + (SELECT person.id AS id + FROM person + WHERE equals(person.team_id, 2) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons + INNER JOIN + (SELECT aggregation_target AS actor_id, + step_1_matching_events AS matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + avg(step_1_conversion_time) AS step_1_average_conversion_time_inner, + avg(step_2_conversion_time) AS step_2_average_conversion_time_inner, + median(step_1_conversion_time) AS step_1_median_conversion_time_inner, + median(step_2_conversion_time) AS step_2_median_conversion_time_inner, + groupArray(10)(step_0_matching_event) AS step_0_matching_events, + groupArray(10)(step_1_matching_event) AS step_1_matching_events, + groupArray(10)(step_2_matching_event) AS step_2_matching_events, + groupArray(10)(final_matching_event) AS final_matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, + step_1_conversion_time AS step_1_conversion_time, + step_2_conversion_time AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + min(latest_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, + min(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS uuid_1, + min(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS `$session_id_1`, + min(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS latest_2, + min(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS uuid_2, + min(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS `$session_id_2`, + min(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS `$window_id_2` + FROM + (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, + e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, + if(equals(e.event, 'step one'), 1, 0) AS step_0, + if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, + if(equals(e.event, 'step two'), 1, 0) AS step_1, + if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, + if(equals(e.event, 'step three'), 1, 0) AS step_2, + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` + FROM events AS e + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-08 23:59:59.999999', 6, 'UTC')))))) + WHERE ifNull(equals(step_0, 1), 0))) + GROUP BY aggregation_target, + steps + HAVING ifNull(equals(steps, max_steps), isNull(steps) + and isNull(max_steps))) + WHERE ifNull(equals(steps, 2), 0) + ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) + ORDER BY persons.id ASC + LIMIT 101 + OFFSET 0 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelStrictStepsPersons.test_strict_funnel_person_recordings.5 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, ['s2']), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_persons.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_persons.ambr index 5c0eb5ea141c7..9dcb793d30329 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_persons.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_persons.ambr @@ -3,72 +3,134 @@ ''' SELECT persons.id, persons.id AS id, - toTimeZone(persons.created_at, 'UTC') AS created_at, - 1 + source.matching_events AS matching_events FROM - (SELECT argMax(person.created_at, person.version) AS created_at, - person.id AS id + (SELECT person.id AS id FROM person WHERE equals(person.team_id, 2) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons INNER JOIN - (SELECT aggregation_target AS actor_id + (SELECT aggregation_target AS actor_id, + step_1_matching_events AS matching_events FROM (SELECT aggregation_target AS aggregation_target, toStartOfDay(timestamp) AS entrance_period_start, - max(steps) AS steps_completed + max(steps) AS steps_completed, + groupArray(10)(step_0_matching_event) AS step_0_matching_events, + groupArray(10)(step_1_matching_event) AS step_1_matching_events, + groupArray(10)(step_2_matching_event) AS step_2_matching_events, + groupArray(10)(final_matching_event) AS final_matching_events FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, step_2 AS step_2, latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target - ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, step_2 AS step_2, - if(ifNull(less(latest_2, latest_1), 0), NULL, latest_2) AS latest_2 + if(ifNull(less(latest_2, latest_1), 0), NULL, latest_2) AS latest_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, uuid_2) AS uuid_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$session_id_2`) AS `$session_id_2`, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$window_id_2`) AS `$window_id_2` FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, - step_2 AS step_2, - min(latest_2) OVER (PARTITION BY aggregation_target - ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 + last_value(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_1, + last_value(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_1`, + last_value(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` FROM (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, if(equals(e.event, 'step one'), 1, 0) AS step_0, if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, if(equals(e.event, 'step two'), 1, 0) AS step_1, if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, if(equals(e.event, 'step three'), 1, 0) AS step_2, - if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2 + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -84,83 +146,159 @@ entrance_period_start) WHERE ifNull(greaterOrEquals(steps_completed, 2), 0) ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) - ORDER BY toTimeZone(persons.created_at, 'UTC') DESC + ORDER BY persons.id ASC LIMIT 101 OFFSET 0 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 ''' # --- +# name: TestFunnelTrendsPersons.test_funnel_trend_persons_returns_recordings.1 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, ['s1b']), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- # name: TestFunnelTrendsPersons.test_funnel_trend_persons_with_drop_off ''' SELECT persons.id, persons.id AS id, - toTimeZone(persons.created_at, 'UTC') AS created_at, - 1 + source.matching_events AS matching_events FROM - (SELECT argMax(person.created_at, person.version) AS created_at, - person.id AS id + (SELECT person.id AS id FROM person WHERE equals(person.team_id, 2) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons INNER JOIN - (SELECT aggregation_target AS actor_id + (SELECT aggregation_target AS actor_id, + final_matching_events AS matching_events FROM (SELECT aggregation_target AS aggregation_target, toStartOfDay(timestamp) AS entrance_period_start, - max(steps) AS steps_completed + max(steps) AS steps_completed, + groupArray(10)(step_0_matching_event) AS step_0_matching_events, + groupArray(10)(step_1_matching_event) AS step_1_matching_events, + groupArray(10)(step_2_matching_event) AS step_2_matching_events, + groupArray(10)(final_matching_event) AS final_matching_events FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, step_2 AS step_2, latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target - ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, step_2 AS step_2, - if(ifNull(less(latest_2, latest_1), 0), NULL, latest_2) AS latest_2 + if(ifNull(less(latest_2, latest_1), 0), NULL, latest_2) AS latest_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, uuid_2) AS uuid_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$session_id_2`) AS `$session_id_2`, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$window_id_2`) AS `$window_id_2` FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, - step_2 AS step_2, - min(latest_2) OVER (PARTITION BY aggregation_target - ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 + last_value(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_1, + last_value(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_1`, + last_value(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` FROM (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, if(equals(e.event, 'step one'), 1, 0) AS step_0, if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, if(equals(e.event, 'step two'), 1, 0) AS step_1, if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, if(equals(e.event, 'step three'), 1, 0) AS step_2, - if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2 + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -176,83 +314,159 @@ entrance_period_start) WHERE and(ifNull(greaterOrEquals(steps_completed, 1), 0), ifNull(less(steps_completed, 3), 0)) ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) - ORDER BY toTimeZone(persons.created_at, 'UTC') DESC + ORDER BY persons.id ASC LIMIT 101 OFFSET 0 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 ''' # --- +# name: TestFunnelTrendsPersons.test_funnel_trend_persons_with_drop_off.1 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, ['s1a']), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- # name: TestFunnelTrendsPersons.test_funnel_trend_persons_with_no_to_step ''' SELECT persons.id, persons.id AS id, - toTimeZone(persons.created_at, 'UTC') AS created_at, - 1 + source.matching_events AS matching_events FROM - (SELECT argMax(person.created_at, person.version) AS created_at, - person.id AS id + (SELECT person.id AS id FROM person WHERE equals(person.team_id, 2) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons INNER JOIN - (SELECT aggregation_target AS actor_id + (SELECT aggregation_target AS actor_id, + final_matching_events AS matching_events FROM (SELECT aggregation_target AS aggregation_target, toStartOfDay(timestamp) AS entrance_period_start, - max(steps) AS steps_completed + max(steps) AS steps_completed, + groupArray(10)(step_0_matching_event) AS step_0_matching_events, + groupArray(10)(step_1_matching_event) AS step_1_matching_events, + groupArray(10)(step_2_matching_event) AS step_2_matching_events, + groupArray(10)(final_matching_event) AS final_matching_events FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, step_2 AS step_2, latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target - ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, step_2 AS step_2, - if(ifNull(less(latest_2, latest_1), 0), NULL, latest_2) AS latest_2 + if(ifNull(less(latest_2, latest_1), 0), NULL, latest_2) AS latest_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, uuid_2) AS uuid_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$session_id_2`) AS `$session_id_2`, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$window_id_2`) AS `$window_id_2` FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, - step_2 AS step_2, - min(latest_2) OVER (PARTITION BY aggregation_target - ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 + last_value(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_1, + last_value(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_1`, + last_value(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` FROM (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, if(equals(e.event, 'step one'), 1, 0) AS step_0, if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, if(equals(e.event, 'step two'), 1, 0) AS step_1, if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, if(equals(e.event, 'step three'), 1, 0) AS step_2, - if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2 + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -268,10 +482,24 @@ entrance_period_start) WHERE ifNull(greaterOrEquals(steps_completed, 3), 0) ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) - ORDER BY toTimeZone(persons.created_at, 'UTC') DESC + ORDER BY persons.id ASC LIMIT 101 OFFSET 0 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 ''' # --- +# name: TestFunnelTrendsPersons.test_funnel_trend_persons_with_no_to_step.1 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, ['s1c']), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered_persons.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered_persons.ambr new file mode 100644 index 0000000000000..6f308a54388e3 --- /dev/null +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered_persons.ambr @@ -0,0 +1,287 @@ +# serializer version: 1 +# name: TestFunnelUnorderedStepsPersons.test_unordered_funnel_does_not_return_recordings + ''' + SELECT persons.id, + persons.id AS id, + source.matching_events AS matching_events + FROM + (SELECT person.id AS id + FROM person + WHERE equals(person.team_id, 2) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons + INNER JOIN + (SELECT aggregation_target AS actor_id, + array() AS matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + avg(step_1_conversion_time) AS step_1_average_conversion_time_inner, + avg(step_2_conversion_time) AS step_2_average_conversion_time_inner, + median(step_1_conversion_time) AS step_1_median_conversion_time_inner, + median(step_2_conversion_time) AS step_2_median_conversion_time_inner + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, + step_1_conversion_time AS step_1_conversion_time, + step_2_conversion_time AS step_2_conversion_time + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, + arraySort([latest_0, latest_1, latest_2]) AS event_times, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySort([latest_0, latest_1, latest_2]) AS conversion_times, + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + min(latest_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, + last_value(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_1, + last_value(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_1`, + last_value(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` + FROM + (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, + e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, + if(equals(e.event, 'step one'), 1, 0) AS step_0, + if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, + if(equals(e.event, 'step two'), 1, 0) AS step_1, + if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, + if(equals(e.event, 'step three'), 1, 0) AS step_2, + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` + FROM events AS e + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-08 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) + WHERE ifNull(equals(step_0, 1), 0) + UNION ALL SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, + arraySort([latest_0, latest_1, latest_2]) AS event_times, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySort([latest_0, latest_1, latest_2]) AS conversion_times, + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + min(latest_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, + last_value(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_1, + last_value(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_1`, + last_value(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` + FROM + (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, + e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, + if(equals(e.event, 'step two'), 1, 0) AS step_0, + if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, + if(equals(e.event, 'step three'), 1, 0) AS step_1, + if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, + if(equals(e.event, 'step one'), 1, 0) AS step_2, + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` + FROM events AS e + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-08 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) + WHERE ifNull(equals(step_0, 1), 0) + UNION ALL SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, + arraySort([latest_0, latest_1, latest_2]) AS event_times, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySort([latest_0, latest_1, latest_2]) AS conversion_times, + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + min(latest_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, + last_value(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_1, + last_value(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_1`, + last_value(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` + FROM + (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, + e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, + if(equals(e.event, 'step three'), 1, 0) AS step_0, + if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, + if(equals(e.event, 'step one'), 1, 0) AS step_1, + if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, + if(equals(e.event, 'step two'), 1, 0) AS step_2, + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` + FROM events AS e + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-08 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) + WHERE ifNull(equals(step_0, 1), 0))) + GROUP BY aggregation_target, + steps + HAVING ifNull(equals(steps, max_steps), isNull(steps) + and isNull(max_steps))) + WHERE ifNull(in(steps, [1, 2, 3]), 0) + ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) + ORDER BY persons.id ASC + LIMIT 101 + OFFSET 0 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelUnorderedStepsPersons.test_unordered_funnel_does_not_return_recordings.1 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, []), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- From 10bdc92fc03fb5bfbb2b5606dc10b4d245358f3c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:48:00 +0000 Subject: [PATCH 13/17] Update UI snapshots for `chromium` (1) --- .../exporter-exporter--dashboard--light.png | Bin 26805 -> 24372 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/exporter-exporter--dashboard--light.png b/frontend/__snapshots__/exporter-exporter--dashboard--light.png index 61f6b619057b78ffd6e149fd37a69988bca977ae..e682dc216ddd3e400d98e145f68dbb877393f521 100644 GIT binary patch literal 24372 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 E17lnVcmMzZ literal 26805 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 HY|#Gz$K1qK From 519fa71ccb6c31d5dbe353c533e86dacb5101eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Tue, 27 Feb 2024 10:53:42 +0100 Subject: [PATCH 14/17] tests --- .../funnels/test/test_funnel_persons.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py index 3e9a02cf18fc3..4c342d2f2926c 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py @@ -391,13 +391,16 @@ def test_first_step_breakdowns(self): } results = get_actors(filters, self.team, funnelStep=1) - self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid, person2.uuid]) + # self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid, person2.uuid]) + self.assertCountEqual([results[0][0], results[1][0]], [person1.uuid, person2.uuid]) results = get_actors(filters, self.team, funnelStep=1, funnelStepBreakdown=["Chrome"]) - self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid]) + # self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid]) + self.assertCountEqual([results[0][0]], [person1.uuid]) results = get_actors(filters, self.team, funnelStep=1, funnelStepBreakdown=["Safari"]) - self.assertCountEqual([val[0]["id"] for val in results], [person2.uuid]) + # self.assertCountEqual([val[0]["id"] for val in results], [person2.uuid]) + self.assertCountEqual([results[0][0]], [person2.uuid]) def test_first_step_breakdowns_with_multi_property_breakdown(self): person1, person2 = self._create_browser_breakdown_events() @@ -417,13 +420,16 @@ def test_first_step_breakdowns_with_multi_property_breakdown(self): } results = get_actors(filters, self.team, funnelStep=1) - self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid, person2.uuid]) + # self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid, person2.uuid]) + self.assertCountEqual([results[0][0], results[1][0]], [person1.uuid, person2.uuid]) results = get_actors(filters, self.team, funnelStep=1, funnelStepBreakdown=["Chrome", "95"]) - self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid]) + # self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid]) + self.assertCountEqual([results[0][0]], [person1.uuid]) results = get_actors(filters, self.team, funnelStep=1, funnelStepBreakdown=["Safari", "14"]) - self.assertCountEqual([val[0]["id"] for val in results], [person2.uuid]) + # self.assertCountEqual([val[0]["id"] for val in results], [person2.uuid]) + self.assertCountEqual([results[0][0]], [person2.uuid]) @also_test_with_materialized_columns(person_properties=["$country"]) def test_first_step_breakdown_person(self): @@ -444,10 +450,12 @@ def test_first_step_breakdown_person(self): } results = get_actors(filters, self.team, funnelStep=1) - self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid, person2.uuid]) + # self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid, person2.uuid]) + self.assertCountEqual([results[0][0], results[1][0]], [person1.uuid, person2.uuid]) results = get_actors(filters, self.team, funnelStep=1, funnelStepBreakdown=["EE"]) - self.assertCountEqual([val[0]["id"] for val in results], [person2.uuid]) + # self.assertCountEqual([val[0]["id"] for val in results], [person2.uuid]) + self.assertCountEqual([results[0][0]], [person2.uuid]) # Check custom_steps give same answers for breakdowns custom_step_results = get_actors( @@ -456,7 +464,8 @@ def test_first_step_breakdown_person(self): self.assertEqual(results, custom_step_results) results = get_actors(filters, self.team, funnelStep=1, funnelStepBreakdown=["PL"]) - self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid]) + # self.assertCountEqual([val[0]["id"] for val in results], [person1.uuid]) + self.assertCountEqual([results[0][0]], [person1.uuid]) # Check custom_steps give same answers for breakdowns custom_step_results = get_actors( @@ -496,7 +505,7 @@ def test_funnel_cohort_breakdown_persons(self): } results = get_actors(filters, self.team, funnelStep=1) - self.assertEqual(results[0][0]["id"], person.uuid) + self.assertEqual(results[0][0], person.uuid) @snapshot_clickhouse_queries @freeze_time("2021-01-02 00:00:00.000Z") From 58112fbe881e04b30eba706ab439130d386b3af9 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:56:42 +0000 Subject: [PATCH 15/17] Update UI snapshots for `chromium` (1) --- .../exporter-exporter--dashboard--light.png | Bin 24372 -> 27611 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/exporter-exporter--dashboard--light.png b/frontend/__snapshots__/exporter-exporter--dashboard--light.png index e682dc216ddd3e400d98e145f68dbb877393f521..5f5193940aa4455a79650929971c87ff0156cf53 100644 GIT binary patch literal 27611 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 Y!gG~smGfU)lJG}=myvdkmh~Th1B&b-y8r+H literal 24372 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 E17lnVcmMzZ From 59c02eb2de7350c48f1d0ab810d71a1d3c1ba95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Tue, 27 Feb 2024 11:29:18 +0100 Subject: [PATCH 16/17] fix types --- .../insights/funnels/test/test_funnel_trends_persons.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py index 5a994ccdd746c..54a8b4cf063ea 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_persons.py @@ -74,7 +74,7 @@ def test_funnel_trend_persons_returns_recordings(self): self.assertEqual(results[0][0], persons["user_one"].uuid) self.assertEqual( # [person["matched_recordings"][0]["session_id"] for person in results], - [list(results[0][2])[0]["session_id"]], # type: ignore + [list(results[0][2])[0]["session_id"]], ["s1b"], ) @@ -124,7 +124,7 @@ def test_funnel_trend_persons_with_no_to_step(self): self.assertEqual(results[0][0], persons["user_one"].uuid) self.assertEqual( # [person["matched_recordings"][0]["session_id"] for person in results], - [list(results[0][2])[0]["session_id"]], # type: ignore + [list(results[0][2])[0]["session_id"]], ["s1c"], ) @@ -163,6 +163,6 @@ def test_funnel_trend_persons_with_drop_off(self): self.assertEqual(results[0][0], persons["user_one"].uuid) self.assertEqual( # [person["matched_recordings"][0].get("session_id") for person in results], - [list(results[0][2])[0]["session_id"]], # type: ignore + [list(results[0][2])[0]["session_id"]], ["s1a"], ) From 9097bcb13529aa6e9834fedcccb004d58fdf094f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:38:44 +0000 Subject: [PATCH 17/17] Update query snapshots --- .../test/__snapshots__/test_database.ambr | 8 + .../test/__snapshots__/test_resolver.ambr | 143 +++++ .../__snapshots__/test_funnel_persons.ambr | 547 ++++++++++++++++++ 3 files changed, 698 insertions(+) create mode 100644 posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons.ambr diff --git a/posthog/hogql/database/test/__snapshots__/test_database.ambr b/posthog/hogql/database/test/__snapshots__/test_database.ambr index b738667e12694..ed6a3075f2d6d 100644 --- a/posthog/hogql/database/test/__snapshots__/test_database.ambr +++ b/posthog/hogql/database/test/__snapshots__/test_database.ambr @@ -35,6 +35,10 @@ "key": "$session_id", "type": "string" }, + { + "key": "$window_id", + "type": "string" + }, { "key": "pdi", "type": "lazy_table", @@ -801,6 +805,10 @@ "key": "$session_id", "type": "string" }, + { + "key": "$window_id", + "type": "string" + }, { "key": "pdi", "type": "lazy_table", diff --git a/posthog/hogql/test/__snapshots__/test_resolver.ambr b/posthog/hogql/test/__snapshots__/test_resolver.ambr index 3e6b3c0f63962..2d84c88ac5f3e 100644 --- a/posthog/hogql/test/__snapshots__/test_resolver.ambr +++ b/posthog/hogql/test/__snapshots__/test_resolver.ambr @@ -28,6 +28,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -91,6 +92,13 @@ table_type: } }, + $window_id: { + alias: "$window_id" + type: { + name: "$window_id" + table_type: + } + }, created_at: { alias: "created_at" type: { @@ -273,6 +281,23 @@ type: } }, + { + alias: "$window_id" + expr: { + chain: [ + "$window_id" + ] + type: { + name: "$window_id" + table_type: + } + } + hidden: True + type: { + alias: "$window_id" + type: + } + }, { alias: "$group_0" expr: { @@ -450,6 +475,17 @@ hidden: True type: }, + { + alias: "$window_id" + expr: { + chain: [ + "$window_id" + ] + type: + } + hidden: True + type: + }, { alias: "$group_0" expr: { @@ -531,6 +567,7 @@ $group_3: , $group_4: , $session_id: , + $window_id: , created_at: , distinct_id: , elements_chain: , @@ -574,6 +611,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -637,6 +675,13 @@ table_type: } }, + $window_id: { + alias: "$window_id" + type: { + name: "$window_id" + table_type: + } + }, created_at: { alias: "created_at" type: { @@ -819,6 +864,23 @@ type: } }, + { + alias: "$window_id" + expr: { + chain: [ + "$window_id" + ] + type: { + name: "$window_id" + table_type: + } + } + hidden: True + type: { + alias: "$window_id" + type: + } + }, { alias: "$group_0" expr: { @@ -998,6 +1060,17 @@ hidden: True type: }, + { + alias: "$window_id" + expr: { + chain: [ + "$window_id" + ] + type: + } + hidden: True + type: + }, { alias: "$group_0" expr: { @@ -1084,6 +1157,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -1237,6 +1311,23 @@ type: } }, + { + alias: "$window_id" + expr: { + chain: [ + "$window_id" + ] + type: { + name: "$window_id" + table_type: + } + } + hidden: True + type: { + alias: "$window_id" + type: + } + }, { alias: "$group_0" expr: { @@ -1342,6 +1433,7 @@ $group_3: , $group_4: , $session_id: , + $window_id: , created_at: , distinct_id: , elements_chain: , @@ -1378,6 +1470,7 @@ $group_3: , $group_4: , $session_id: , + $window_id: , created_at: , distinct_id: , elements_chain: , @@ -1621,6 +1714,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -1774,6 +1868,23 @@ type: } }, + { + alias: "$window_id" + expr: { + chain: [ + "$window_id" + ] + type: { + name: "$window_id" + table_type: + } + } + hidden: True + type: { + alias: "$window_id" + type: + } + }, { alias: "$group_0" expr: { @@ -1879,6 +1990,7 @@ $group_3: , $group_4: , $session_id: , + $window_id: , created_at: , distinct_id: , elements_chain: , @@ -1918,6 +2030,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -2072,6 +2185,23 @@ type: } }, + { + alias: "$window_id" + expr: { + chain: [ + "$window_id" + ] + type: { + name: "$window_id" + table_type: + } + } + hidden: True + type: { + alias: "$window_id" + type: + } + }, { alias: "$group_0" expr: { @@ -2178,6 +2308,7 @@ $group_3: , $group_4: , $session_id: , + $window_id: , created_at: , distinct_id: , elements_chain: , @@ -2217,6 +2348,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -2473,6 +2605,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -2603,6 +2736,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -2737,6 +2871,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -2907,6 +3042,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -3082,6 +3218,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -3215,6 +3352,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -3349,6 +3487,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -3464,6 +3603,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -3672,6 +3812,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -3765,6 +3906,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, @@ -3870,6 +4012,7 @@ $group_3: {}, $group_4: {}, $session_id: {}, + $window_id: {}, created_at: {}, distinct_id: {}, elements_chain: {}, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons.ambr new file mode 100644 index 0000000000000..a9a6fc5357f72 --- /dev/null +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons.ambr @@ -0,0 +1,547 @@ +# serializer version: 1 +# name: TestFunnelPersons.test_funnel_person_recordings + ''' + SELECT persons.id, + persons.id AS id, + source.matching_events AS matching_events + FROM + (SELECT person.id AS id + FROM person + WHERE equals(person.team_id, 2) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons + INNER JOIN + (SELECT aggregation_target AS actor_id, + step_0_matching_events AS matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + avg(step_1_conversion_time) AS step_1_average_conversion_time_inner, + avg(step_2_conversion_time) AS step_2_average_conversion_time_inner, + median(step_1_conversion_time) AS step_1_median_conversion_time_inner, + median(step_2_conversion_time) AS step_2_median_conversion_time_inner, + groupArray(10)(step_0_matching_event) AS step_0_matching_events, + groupArray(10)(step_1_matching_event) AS step_1_matching_events, + groupArray(10)(step_2_matching_event) AS step_2_matching_events, + groupArray(10)(final_matching_event) AS final_matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, + step_1_conversion_time AS step_1_conversion_time, + step_2_conversion_time AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, latest_2) AS latest_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, uuid_2) AS uuid_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$session_id_2`) AS `$session_id_2`, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$window_id_2`) AS `$window_id_2` + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + min(latest_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, + last_value(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_1, + last_value(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_1`, + last_value(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` + FROM + (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, + e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, + if(equals(e.event, 'step one'), 1, 0) AS step_0, + if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, + if(equals(e.event, 'step two'), 1, 0) AS step_1, + if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, + if(equals(e.event, 'step three'), 1, 0) AS step_2, + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` + FROM events AS e + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-08 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))))) + WHERE ifNull(equals(step_0, 1), 0))) + GROUP BY aggregation_target, + steps + HAVING ifNull(equals(steps, max_steps), isNull(steps) + and isNull(max_steps))) + WHERE ifNull(in(steps, [1, 2, 3]), 0) + ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) + ORDER BY persons.id ASC + LIMIT 101 + OFFSET 0 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelPersons.test_funnel_person_recordings.1 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, ['s1']), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelPersons.test_funnel_person_recordings.2 + ''' + SELECT persons.id, + persons.id AS id, + source.matching_events AS matching_events + FROM + (SELECT person.id AS id + FROM person + WHERE equals(person.team_id, 2) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons + INNER JOIN + (SELECT aggregation_target AS actor_id, + step_1_matching_events AS matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + avg(step_1_conversion_time) AS step_1_average_conversion_time_inner, + avg(step_2_conversion_time) AS step_2_average_conversion_time_inner, + median(step_1_conversion_time) AS step_1_median_conversion_time_inner, + median(step_2_conversion_time) AS step_2_median_conversion_time_inner, + groupArray(10)(step_0_matching_event) AS step_0_matching_events, + groupArray(10)(step_1_matching_event) AS step_1_matching_events, + groupArray(10)(step_2_matching_event) AS step_2_matching_events, + groupArray(10)(final_matching_event) AS final_matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, + step_1_conversion_time AS step_1_conversion_time, + step_2_conversion_time AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, latest_2) AS latest_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, uuid_2) AS uuid_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$session_id_2`) AS `$session_id_2`, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$window_id_2`) AS `$window_id_2` + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + min(latest_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, + last_value(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_1, + last_value(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_1`, + last_value(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` + FROM + (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, + e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, + if(equals(e.event, 'step one'), 1, 0) AS step_0, + if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, + if(equals(e.event, 'step two'), 1, 0) AS step_1, + if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, + if(equals(e.event, 'step three'), 1, 0) AS step_2, + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` + FROM events AS e + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-08 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))))) + WHERE ifNull(equals(step_0, 1), 0))) + GROUP BY aggregation_target, + steps + HAVING ifNull(equals(steps, max_steps), isNull(steps) + and isNull(max_steps))) + WHERE ifNull(in(steps, [2, 3]), 0) + ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) + ORDER BY persons.id ASC + LIMIT 101 + OFFSET 0 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelPersons.test_funnel_person_recordings.3 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, ['s2']), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelPersons.test_funnel_person_recordings.4 + ''' + SELECT persons.id, + persons.id AS id, + source.matching_events AS matching_events + FROM + (SELECT person.id AS id + FROM person + WHERE equals(person.team_id, 2) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS persons + INNER JOIN + (SELECT aggregation_target AS actor_id, + step_1_matching_events AS matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + avg(step_1_conversion_time) AS step_1_average_conversion_time_inner, + avg(step_2_conversion_time) AS step_2_average_conversion_time_inner, + median(step_1_conversion_time) AS step_1_median_conversion_time_inner, + median(step_2_conversion_time) AS step_2_median_conversion_time_inner, + groupArray(10)(step_0_matching_event) AS step_0_matching_events, + groupArray(10)(step_1_matching_event) AS step_1_matching_events, + groupArray(10)(step_2_matching_event) AS step_2_matching_events, + groupArray(10)(final_matching_event) AS final_matching_events + FROM + (SELECT aggregation_target AS aggregation_target, + steps AS steps, + max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, + step_1_conversion_time AS step_1_conversion_time, + step_2_conversion_time AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + latest_2 AS latest_2, + uuid_2 AS uuid_2, + `$session_id_2` AS `$session_id_2`, + `$window_id_2` AS `$window_id_2`, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, + tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, + tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, + if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, if(isNull(latest_2), step_1_matching_event, step_2_matching_event))) AS final_matching_event + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + latest_1 AS latest_1, + uuid_1 AS uuid_1, + `$session_id_1` AS `$session_id_1`, + `$window_id_1` AS `$window_id_1`, + step_2 AS step_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, latest_2) AS latest_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, uuid_2) AS uuid_2, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$session_id_2`) AS `$session_id_2`, + if(ifNull(less(latest_2, latest_1), 0), NULL, `$window_id_2`) AS `$window_id_2` + FROM + (SELECT aggregation_target AS aggregation_target, + timestamp AS timestamp, + step_0 AS step_0, + latest_0 AS latest_0, + uuid_0 AS uuid_0, + `$session_id_0` AS `$session_id_0`, + `$window_id_0` AS `$window_id_0`, + step_1 AS step_1, + min(latest_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, + last_value(uuid_1) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_1, + last_value(`$session_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_1`, + last_value(`$window_id_1`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_1`, + step_2 AS step_2, + min(latest_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2, + last_value(uuid_2) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS uuid_2, + last_value(`$session_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$session_id_2`, + last_value(`$window_id_2`) OVER (PARTITION BY aggregation_target + ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS `$window_id_2` + FROM + (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, + e__pdi.person_id AS aggregation_target, + e.uuid AS uuid, + if(equals(e.event, 'step one'), 1, 0) AS step_0, + if(ifNull(equals(step_0, 1), 0), timestamp, NULL) AS latest_0, + if(ifNull(equals(step_0, 1), 0), uuid, NULL) AS uuid_0, + if(ifNull(equals(step_0, 1), 0), e.`$session_id`, NULL) AS `$session_id_0`, + if(ifNull(equals(step_0, 1), 0), e.`$window_id`, NULL) AS `$window_id_0`, + if(equals(e.event, 'step two'), 1, 0) AS step_1, + if(ifNull(equals(step_1, 1), 0), timestamp, NULL) AS latest_1, + if(ifNull(equals(step_1, 1), 0), uuid, NULL) AS uuid_1, + if(ifNull(equals(step_1, 1), 0), e.`$session_id`, NULL) AS `$session_id_1`, + if(ifNull(equals(step_1, 1), 0), e.`$window_id`, NULL) AS `$window_id_1`, + if(equals(e.event, 'step three'), 1, 0) AS step_2, + if(ifNull(equals(step_2, 1), 0), timestamp, NULL) AS latest_2, + if(ifNull(equals(step_2, 1), 0), uuid, NULL) AS uuid_2, + if(ifNull(equals(step_2, 1), 0), e.`$session_id`, NULL) AS `$session_id_2`, + if(ifNull(equals(step_2, 1), 0), e.`$window_id`, NULL) AS `$window_id_2` + FROM events AS e + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-01-08 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))))) + WHERE ifNull(equals(step_0, 1), 0))) + GROUP BY aggregation_target, + steps + HAVING ifNull(equals(steps, max_steps), isNull(steps) + and isNull(max_steps))) + WHERE ifNull(equals(steps, 2), 0) + ORDER BY aggregation_target ASC) AS source ON equals(persons.id, source.actor_id) + ORDER BY persons.id ASC + LIMIT 101 + OFFSET 0 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# --- +# name: TestFunnelPersons.test_funnel_person_recordings.5 + ''' + SELECT DISTINCT session_replay_events.session_id AS session_id + FROM + (SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 2) + GROUP BY session_replay_events.session_id) AS session_replay_events + WHERE ifNull(in(session_replay_events.session_id, ['s2']), 0) + LIMIT 100 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1 + ''' +# ---