Skip to content

Commit

Permalink
feat(hogql): add funnel hogql actors (#20472)
Browse files Browse the repository at this point in the history
  • Loading branch information
thmsobrmlr authored Feb 22, 2024
1 parent 6cd02e9 commit c88988c
Show file tree
Hide file tree
Showing 19 changed files with 1,379 additions and 476 deletions.
64 changes: 62 additions & 2 deletions frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@
{
"$ref": "#/definitions/InsightActorsQuery"
},
{
"$ref": "#/definitions/FunnelsActorsQuery"
},
{
"$ref": "#/definitions/HogQLQuery"
}
Expand Down Expand Up @@ -1692,6 +1695,41 @@
"enum": ["steps", "time_to_convert", "trends"],
"type": "string"
},
"FunnelsActorsQuery": {
"additionalProperties": false,
"properties": {
"funnelCustomSteps": {
"description": "Custom step numbers to get persons for. This overrides `funnelStep`.",
"items": {
"type": "integer"
},
"type": "array"
},
"funnelStep": {
"description": "Index of the step for which we want to get the timestamp for, per person. Positive for converted persons, negative for dropped of persons.",
"type": "integer"
},
"funnelStepBreakdown": {
"$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."
},
"includeRecordings": {
"type": "boolean"
},
"kind": {
"const": "InsightActorsQuery",
"type": "string"
},
"response": {
"$ref": "#/definitions/ActorsQueryResponse"
},
"source": {
"$ref": "#/definitions/FunnelsQuery"
}
},
"required": ["kind", "source"],
"type": "object"
},
"FunnelsFilter": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -2303,8 +2341,11 @@
}
]
},
"includeRecordings": {
"type": "boolean"
},
"interval": {
"description": "An interval selected out of available intervals in source query",
"description": "An interval selected out of available intervals in source query.",
"type": "integer"
},
"kind": {
Expand All @@ -2327,6 +2368,18 @@
"required": ["kind", "source"],
"type": "object"
},
"InsightActorsQueryBase": {
"additionalProperties": false,
"properties": {
"includeRecordings": {
"type": "boolean"
},
"response": {
"$ref": "#/definitions/ActorsQueryResponse"
}
},
"type": "object"
},
"InsightActorsQueryOptions": {
"additionalProperties": false,
"properties": {
Expand All @@ -2338,7 +2391,14 @@
"$ref": "#/definitions/InsightActorsQueryOptionsResponse"
},
"source": {
"$ref": "#/definitions/InsightActorsQuery"
"anyOf": [
{
"$ref": "#/definitions/InsightActorsQuery"
},
{
"$ref": "#/definitions/FunnelsActorsQuery"
}
]
}
},
"required": ["kind", "source"],
Expand Down
29 changes: 21 additions & 8 deletions frontend/src/queries/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,7 @@ export interface ActorsQueryResponse {

export interface ActorsQuery extends DataNode {
kind: NodeKind.ActorsQuery
source?: InsightActorsQuery | HogQLQuery
source?: InsightActorsQuery | FunnelsActorsQuery | HogQLQuery
select?: HogQLExpression[]
search?: string
properties?: AnyPropertyFilter[]
Expand Down Expand Up @@ -1061,20 +1061,33 @@ export type InsightFilter =

export type Day = integer

export interface InsightActorsQuery<T extends InsightsQueryBase = InsightQuerySource> {
export interface InsightActorsQueryBase {
includeRecordings?: boolean
response?: ActorsQueryResponse
}
export interface InsightActorsQuery<T extends InsightsQueryBase = InsightQuerySource> extends InsightActorsQueryBase {
kind: NodeKind.InsightActorsQuery
source: T
day?: string | Day
status?: string
/**
* An interval selected out of available intervals in source query
*/
/** An interval selected out of available intervals in source query. */
interval?: integer
series?: integer
breakdown?: string | BreakdownValueInt
compare?: 'current' | 'previous'
// TODO: add fields for other insights (funnels dropdown, compare_previous choice, etc)
response?: ActorsQueryResponse
}

export interface FunnelsActorsQuery extends InsightActorsQueryBase {
kind: NodeKind.InsightActorsQuery
source: FunnelsQuery
/** 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`. */
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
}

export type BreakdownValueInt = integer
Expand Down Expand Up @@ -1105,7 +1118,7 @@ export interface InsightActorsQueryOptionsResponse {

export interface InsightActorsQueryOptions {
kind: NodeKind.InsightActorsQueryOptions
source: InsightActorsQuery
source: InsightActorsQuery | FunnelsActorsQuery
response?: InsightActorsQueryOptionsResponse
}

Expand Down
83 changes: 60 additions & 23 deletions frontend/src/scenes/funnels/funnelPersonsModalLogic.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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'
import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal'

import { FunnelsActorsQuery, NodeKind } from '~/queries/schema'
import {
FunnelCorrelation,
FunnelCorrelationResultsType,
Expand Down Expand Up @@ -33,7 +36,9 @@ export const funnelPersonsModalLogic = kea<funnelPersonsModalLogicType>([
insightLogic(props),
['isInDashboardContext', 'isInExperimentContext'],
funnelDataLogic(props),
['steps', 'funnelsFilter'],
['steps', 'querySource', 'funnelsFilter'],
featureFlagLogic,
['featureFlags'],
],
})),

Expand Down Expand Up @@ -77,6 +82,12 @@ 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 All @@ -85,36 +96,62 @@ export const funnelPersonsModalLogic = kea<funnelPersonsModalLogicType>([
return
}

openPersonsModal({
// openPersonsModalForStep is for the baseline - for breakdown series use openPersonsModalForSeries
url: generateBaselineConversionUrl(converted ? step.converted_people_url : step.dropped_people_url),
title: funnelTitle({
converted,
// Note - when in a legend the step.order is always 0 so we use stepIndex instead
step: typeof stepIndex === 'number' ? stepIndex + 1 : step.order + 1,
label: step.name,
seriesId: step.order,
order_type: values.funnelsFilter?.funnelOrderType,
}),
// Note - when in a legend the step.order is always 0 so we use stepIndex instead
const stepNo = typeof stepIndex === 'number' ? stepIndex + 1 : step.order + 1
const title = funnelTitle({
converted,
step: stepNo,
label: step.name,
seriesId: step.order,
order_type: values.funnelsFilter?.funnelOrderType,
})

// openPersonsModalForStep is for the baseline - for breakdown series use openPersonsModalForSeries
if (values.hogQLInsightsFunnelsFlagEnabled) {
const query: FunnelsActorsQuery = {
kind: NodeKind.InsightActorsQuery,
source: values.querySource!,
funnelStep: converted ? stepNo : -stepNo,
}
openPersonsModal({ title, query })
} else {
openPersonsModal({
url: generateBaselineConversionUrl(converted ? step.converted_people_url : step.dropped_people_url),
title,
})
}
},
openPersonsModalForSeries: ({ step, series, converted }) => {
if (values.isInDashboardContext) {
return
}
// Version of openPersonsModalForStep that accurately handles breakdown series

const stepNo = step.order + 1
const breakdownValues = getBreakdownStepValues(series, series.order)
openPersonsModal({
url: converted ? series.converted_people_url : series.dropped_people_url,
title: funnelTitle({
converted,
step: step.order + 1,
breakdown_value: breakdownValues.isEmpty ? undefined : breakdownValues.breakdown_value.join(', '),
label: step.name,
seriesId: step.order,
order_type: values.funnelsFilter?.funnelOrderType,
}),
const title = funnelTitle({
converted,
step: stepNo,
breakdown_value: breakdownValues.isEmpty ? undefined : breakdownValues.breakdown_value.join(', '),
label: step.name,
seriesId: step.order,
order_type: values.funnelsFilter?.funnelOrderType,
})

// Version of openPersonsModalForStep that accurately handles breakdown series
if (values.hogQLInsightsFunnelsFlagEnabled) {
const query: FunnelsActorsQuery = {
kind: NodeKind.InsightActorsQuery,
source: values.querySource!,
funnelStep: converted ? stepNo : -stepNo,
funnelStepBreakdown: series.breakdown_value,
}
openPersonsModal({ title, query })
} else {
openPersonsModal({
url: converted ? series.converted_people_url : series.dropped_people_url,
title,
})
}
},
openCorrelationPersonsModal: ({ correlation, success }) => {
if (values.isInDashboardContext) {
Expand Down
62 changes: 62 additions & 0 deletions posthog/hogql_queries/insights/funnels/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,68 @@ def _build_step_query(
else:
return event_expr

def _get_timestamp_outer_select(self) -> List[ast.Expr]:
return []
# if self._include_preceding_timestamp:
# return ", max_timestamp, min_timestamp"
# elif self._include_timestamp:
# return ", timestamp"
# else:
# return ""

def _get_funnel_person_step_condition(self) -> ast.Expr:
actorsQuery, breakdownType, max_steps = (
self.context.actorsQuery,
self.context.breakdownType,
self.context.max_steps,
)
assert actorsQuery is not None

funnelStep = actorsQuery.funnelStep
funnelCustomSteps = actorsQuery.funnelCustomSteps
funnelStepBreakdown = actorsQuery.funnelStepBreakdown

conditions: List[ast.Expr] = []

if funnelCustomSteps:
conditions.append(parse_expr(f"steps IN {funnelCustomSteps}"))
elif funnelStep is not None:
if funnelStep >= 0:
step_nums = [i for i in range(funnelStep, max_steps + 1)]
conditions.append(parse_expr(f"steps IN {step_nums}"))
else:
step_num = abs(funnelStep) - 1
conditions.append(parse_expr(f"steps = {step_num}"))
else:
raise ValueError("Missing both funnelStep and funnelCustomSteps")

if funnelStepBreakdown is not None:
breakdown_prop_value = funnelStepBreakdown
if isinstance(breakdown_prop_value, int) and breakdownType != "cohort":
breakdown_prop_value = str(breakdown_prop_value)

conditions.append(parse_expr(f"arrayFlatten(array(prop)) = arrayFlatten(array({breakdown_prop_value}))"))

return ast.And(exprs=conditions)

def _get_funnel_person_step_events(self) -> List[ast.Expr]:
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] = []

Expand Down
28 changes: 28 additions & 0 deletions posthog/hogql_queries/insights/funnels/funnel_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 import Funnel


class FunnelActors(Funnel):
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
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
BreakdownFilter,
BreakdownType,
FunnelConversionWindowTimeUnit,
FunnelsActorsQuery,
FunnelsFilter,
FunnelsQuery,
HogQLQueryModifiers,
Expand All @@ -31,6 +32,8 @@ class FunnelQueryContext(QueryContext):
funnelWindowInterval: int
funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit

actorsQuery: FunnelsActorsQuery | None

def __init__(
self,
query: FunnelsQuery,
Expand Down
Loading

0 comments on commit c88988c

Please sign in to comment.