Skip to content

Commit

Permalink
feat(hogql): add remaining funnel actors (#20496)
Browse files Browse the repository at this point in the history
  • Loading branch information
thmsobrmlr authored Feb 26, 2024
1 parent 4a12660 commit da1d9fb
Show file tree
Hide file tree
Showing 20 changed files with 1,178 additions and 99 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1699,7 +1699,7 @@
"additionalProperties": false,
"properties": {
"funnelCustomSteps": {
"description": "Custom step numbers to get persons for. This overrides `funnelStep`.",
"description": "Custom step numbers to get persons for. This overrides `funnelStep`. Primarily for correlation use.",
"items": {
"type": "integer"
},
Expand All @@ -1713,6 +1713,13 @@
"$ref": "#/definitions/BreakdownKeyType",
"description": "The breakdown value for which to get persons for. This is an array for person and event properties, a string for groups and an integer for cohorts."
},
"funnelTrendsDropOff": {
"type": "boolean"
},
"funnelTrendsEntrancePeriodStart": {
"description": "Used together with `funnelTrendsDropOff` for funnels time conversion date for the persons modal.",
"type": "string"
},
"includeRecordings": {
"type": "boolean"
},
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/queries/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,11 +1083,14 @@ export interface FunnelsActorsQuery extends InsightActorsQueryBase {
/** Index of the step for which we want to get the timestamp for, per person.
* Positive for converted persons, negative for dropped of persons. */
funnelStep?: integer
/** Custom step numbers to get persons for. This overrides `funnelStep`. */
/** Custom step numbers to get persons for. This overrides `funnelStep`. Primarily for correlation use. */
funnelCustomSteps?: integer[]
/** The breakdown value for which to get persons for. This is an array for
* person and event properties, a string for groups and an integer for cohorts. */
funnelStepBreakdown?: BreakdownKeyType
funnelTrendsDropOff?: boolean
/** Used together with `funnelTrendsDropOff` for funnels time conversion date for the persons modal. */
funnelTrendsEntrancePeriodStart?: string
}

export type BreakdownValueInt = integer
Expand Down
47 changes: 33 additions & 14 deletions frontend/src/scenes/funnels/FunnelLineGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { buildPeopleUrl } from 'scenes/trends/persons-modal/persons-modal-utils'
import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal'

import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter'
import { TrendsFilter } from '~/queries/schema'
import { FunnelsActorsQuery, NodeKind, TrendsFilter } from '~/queries/schema'
import { isInsightQueryNode } from '~/queries/utils'
import { ChartParams, GraphDataset, GraphType } from '~/types'

Expand All @@ -28,8 +28,15 @@ export function FunnelLineGraph({
showPersonsModal = true,
}: Omit<ChartParams, 'filters'>): JSX.Element | null {
const { insightProps } = useValues(insightLogic)
const { indexedSteps, aggregationTargetLabel, incompletenessOffsetFromEnd, interval, querySource, insightData } =
useValues(funnelDataLogic(insightProps))
const {
hogQLInsightsFunnelsFlagEnabled,
indexedSteps,
aggregationTargetLabel,
incompletenessOffsetFromEnd,
interval,
querySource,
insightData,
} = useValues(funnelDataLogic(insightProps))

if (!isInsightQueryNode(querySource)) {
return null
Expand Down Expand Up @@ -78,19 +85,31 @@ export function FunnelLineGraph({
const day = dataset?.days?.[index] ?? ''
const label = dataset?.label ?? dataset?.labels?.[index] ?? ''

const filters = queryNodeToFilter(querySource) // for persons modal
const personsUrl = buildPeopleUrl({
filters,
date_from: day ?? '',
response: insightData,
})
if (personsUrl) {
const title = `${capitalizeFirstLetter(
aggregationTargetLabel.plural
)} converted on ${dayjs(label).format('MMMM Do YYYY')}`

if (hogQLInsightsFunnelsFlagEnabled) {
const query: FunnelsActorsQuery = {
kind: NodeKind.InsightActorsQuery,
source: querySource,
funnelTrendsDropOff: false,
funnelTrendsEntrancePeriodStart: dayjs(day).format('YYYY-MM-DD HH:mm:ss'),
}
openPersonsModal({
url: personsUrl,
title: `${capitalizeFirstLetter(
aggregationTargetLabel.plural
)} converted on ${dayjs(label).format('MMMM Do YYYY')}`,
title,
query,
})
} else {
const filters = queryNodeToFilter(querySource) // for persons modal
const personsUrl = buildPeopleUrl({
filters,
date_from: day ?? '',
response: insightData,
})
if (personsUrl) {
openPersonsModal({ title, url: personsUrl })
}
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/scenes/funnels/funnelDataLogic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { actions, connect, kea, key, path, props, reducers, selectors } from 'kea'
import { BIN_COUNT_AUTO } from 'lib/constants'
import { BIN_COUNT_AUTO, FEATURE_FLAGS } from 'lib/constants'
import { dayjs } from 'lib/dayjs'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { average, percentage, sum } from 'lib/utils'
import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic'
import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils'
Expand Down Expand Up @@ -61,6 +62,8 @@ export const funnelDataLogic = kea<funnelDataLogicType>([
],
groupsModel,
['aggregationLabel'],
featureFlagLogic,
['featureFlags'],
],
actions: [insightVizDataLogic(props), ['updateInsightFilter', 'updateQuerySource']],
})),
Expand All @@ -79,6 +82,12 @@ export const funnelDataLogic = kea<funnelDataLogicType>([
}),

selectors(() => ({
hogQLInsightsFunnelsFlagEnabled: [
(s) => [s.featureFlags],
(featureFlags): boolean => {
return !!featureFlags[FEATURE_FLAGS.HOGQL_INSIGHTS_FUNNELS]
},
],
querySource: [
(s) => [s.vizQuerySource],
(vizQuerySource) => (isFunnelsQuery(vizQuerySource) ? vizQuerySource : null),
Expand Down
12 changes: 1 addition & 11 deletions frontend/src/scenes/funnels/funnelPersonsModalLogic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { actions, connect, kea, key, listeners, path, props, selectors } from 'kea'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { insightLogic } from 'scenes/insights/insightLogic'
import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils'
import { funnelTitle } from 'scenes/trends/persons-modal/persons-modal-utils'
Expand Down Expand Up @@ -36,9 +34,7 @@ export const funnelPersonsModalLogic = kea<funnelPersonsModalLogicType>([
insightLogic(props),
['isInDashboardContext', 'isInExperimentContext'],
funnelDataLogic(props),
['steps', 'querySource', 'funnelsFilter'],
featureFlagLogic,
['featureFlags'],
['hogQLInsightsFunnelsFlagEnabled', 'steps', 'querySource', 'funnelsFilter'],
],
})),

Expand Down Expand Up @@ -82,12 +78,6 @@ export const funnelPersonsModalLogic = kea<funnelPersonsModalLogicType>([
return !isInDashboardContext && !funnelsFilter?.funnelAggregateByHogQL
},
],
hogQLInsightsFunnelsFlagEnabled: [
(s) => [s.featureFlags],
(featureFlags): boolean => {
return !!featureFlags[FEATURE_FLAGS.HOGQL_INSIGHTS_FUNNELS]
},
],
}),

listeners(({ values }) => ({
Expand Down
4 changes: 4 additions & 0 deletions posthog/hogql_queries/insights/funnels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
from .funnel_unordered import FunnelUnordered
from .funnel_time_to_convert import FunnelTimeToConvert
from .funnel_trends import FunnelTrends
from .funnel_persons import FunnelActors
from .funnel_strict_persons import FunnelStrictActors
from .funnel_unordered_persons import FunnelUnorderedActors
from .funnel_trends_persons import FunnelTrendsActors
28 changes: 28 additions & 0 deletions posthog/hogql_queries/insights/funnels/funnel_strict_persons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from typing import List

from posthog.hogql import ast
from posthog.hogql_queries.insights.funnels.funnel_strict import FunnelStrict


class FunnelStrictActors(FunnelStrict):
def actor_query(
self,
# extra_fields: Optional[List[str]] = None,
) -> ast.SelectQuery:
select: List[ast.Expr] = [
ast.Alias(alias="actor_id", expr=ast.Field(chain=["aggregation_target"])),
*self._get_funnel_person_step_events(),
*self._get_timestamp_outer_select(),
# {extra_fields}
]
select_from = ast.JoinExpr(table=self.get_step_counts_query())
where = self._get_funnel_person_step_condition()
order_by = [ast.OrderExpr(expr=ast.Field(chain=["aggregation_target"]))]

return ast.SelectQuery(
select=select,
select_from=select_from,
order_by=order_by,
where=where,
# SETTINGS max_ast_elements=1000000, max_expanded_ast_elements=1000000
)
17 changes: 11 additions & 6 deletions posthog/hogql_queries/insights/funnels/funnel_trends.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,6 @@ def get_step_counts_without_aggregation_query(

steps_per_person_query = self.funnel_order.get_step_counts_without_aggregation_query()

# # This is used by funnel trends when we only need data for one period, e.g. person per data point
# if specific_entrance_period_start:
# self.params["entrance_period_start"] = specific_entrance_period_start.strftime(TIMESTAMP_FORMAT)

# event_select_clause = ""
# if self._filter.include_recordings:
# max_steps = len(self._filter.entities)
Expand All @@ -291,14 +287,23 @@ def get_step_counts_without_aggregation_query(
*breakdown_clause,
]
select_from = ast.JoinExpr(table=steps_per_person_query)
# {"WHERE toDateTime(entrance_period_start) = %(entrance_period_start)s" if specific_entrance_period_start else ""}
# This is used by funnel trends when we only need data for one period, e.g. person per data point
where = (
ast.CompareOperation(
op=ast.CompareOperationOp.Eq,
left=parse_expr("entrance_period_start"),
right=ast.Constant(value=specific_entrance_period_start),
)
if specific_entrance_period_start
else None
)
group_by: List[ast.Expr] = [
ast.Field(chain=["aggregation_target"]),
ast.Field(chain=["entrance_period_start"]),
*breakdown_clause,
]

return ast.SelectQuery(select=select, select_from=select_from, group_by=group_by)
return ast.SelectQuery(select=select, select_from=select_from, where=where, group_by=group_by)

def get_steps_reached_conditions(self) -> Tuple[str, str, str]:
funnelsFilter, max_steps = self.context.funnelsFilter, self.context.max_steps
Expand Down
87 changes: 87 additions & 0 deletions posthog/hogql_queries/insights/funnels/funnel_trends_persons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from datetime import datetime
from typing import List

from rest_framework.exceptions import ValidationError

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.funnels.funnel_trends import FunnelTrends
from posthog.utils import relative_date_parse


class FunnelTrendsActors(FunnelTrends):
entrancePeriodStart: datetime
dropOff: bool

def __init__(self, context: FunnelQueryContext, just_summarize=False):
super().__init__(context, just_summarize)

team, actorsQuery = self.context.team, self.context.actorsQuery

if actorsQuery is None:
raise ValidationError("No actors query present.")

if actorsQuery.funnelTrendsDropOff is None:
raise ValidationError(f"Actors parameter `funnelTrendsDropOff` must be provided for funnel trends persons!")

if actorsQuery.funnelTrendsEntrancePeriodStart is None:
raise ValidationError(
f"Actors parameter `funnelTrendsEntrancePeriodStart` must be provided funnel trends persons!"
)

entrancePeriodStart = relative_date_parse(actorsQuery.funnelTrendsEntrancePeriodStart, team.timezone_info)
if entrancePeriodStart is None:
raise ValidationError(
f"Actors parameter `funnelTrendsEntrancePeriodStart` must be a valid relative date string!"
)

self.dropOff = actorsQuery.funnelTrendsDropOff
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"
return []

def actor_query(self) -> ast.SelectQuery:
step_counts_query = self.get_step_counts_without_aggregation_query(
specific_entrance_period_start=self.entrancePeriodStart
)

# Expects multiple rows for same person, first event time, steps taken.
(
_,
reached_to_step_count_condition,
did_not_reach_to_step_count_condition,
) = self.get_steps_reached_conditions()

select: List[ast.Expr] = [
ast.Alias(alias="actor_id", expr=ast.Field(chain=["aggregation_target"])),
*self._get_funnel_person_step_events(),
]
select_from = ast.JoinExpr(table=step_counts_query)
where = (
parse_expr(did_not_reach_to_step_count_condition)
if self.dropOff
else parse_expr(reached_to_step_count_condition)
)
order_by = [ast.OrderExpr(expr=ast.Field(chain=["aggregation_target"]))]

return ast.SelectQuery(
select=select,
select_from=select_from,
order_by=order_by,
where=where,
# SETTINGS max_ast_elements=1000000, max_expanded_ast_elements=1000000
)
37 changes: 37 additions & 0 deletions posthog/hogql_queries/insights/funnels/funnel_unordered_persons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import List

from posthog.hogql import ast
from posthog.hogql.parser import parse_expr
from posthog.hogql_queries.insights.funnels.funnel_unordered import FunnelUnordered


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:
return [parse_expr("array() as matching_events")] # type: ignore
return []

def actor_query(
self,
# extra_fields: Optional[List[str]] = None,
) -> ast.SelectQuery:
select: List[ast.Expr] = [
ast.Alias(alias="actor_id", expr=ast.Field(chain=["aggregation_target"])),
*self._get_funnel_person_step_events(),
*self._get_timestamp_outer_select(),
# {extra_fields}
]
select_from = ast.JoinExpr(table=self.get_step_counts_query())
where = self._get_funnel_person_step_condition()
order_by = [ast.OrderExpr(expr=ast.Field(chain=["aggregation_target"]))]

return ast.SelectQuery(
select=select,
select_from=select_from,
order_by=order_by,
where=where,
# SETTINGS max_ast_elements=1000000, max_expanded_ast_elements=1000000
)
Loading

0 comments on commit da1d9fb

Please sign in to comment.