Skip to content

Commit

Permalink
Merge branch 'master' into no_analyze
Browse files Browse the repository at this point in the history
  • Loading branch information
fuziontech authored Nov 26, 2024
2 parents 109c8a1 + 7a347c3 commit b71ae75
Show file tree
Hide file tree
Showing 26 changed files with 314 additions and 72 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions frontend/src/lib/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,14 @@ export const cohortOperatorMap: Record<string, string> = {
not_in: 'user not in',
}

export const stickinessOperatorMap: Record<string, string> = {
exact: 'Exactly',
gte: 'At least',
lte: 'At most (but at least once)',
}

export const allOperatorsMapping: Record<string, string> = {
...stickinessOperatorMap,
...dateTimeOperatorMap,
...stringOperatorMap,
...numericOperatorMap,
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/queries/nodes/InsightViz/EditorFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { insightLogic } from 'scenes/insights/insightLogic'
import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic'
import { userLogic } from 'scenes/userLogic'

import { StickinessCriteria } from '~/queries/nodes/InsightViz/StickinessCriteria'
import { InsightQueryNode } from '~/queries/schema'
import {
AvailableFeature,
Expand Down Expand Up @@ -56,6 +57,7 @@ export function EditorFilters({ query, showing, embedded }: EditorFiltersProps):
isRetention,
isPaths,
isLifecycle,
isStickiness,
isTrendsLike,
display,
breakdownFilter,
Expand Down Expand Up @@ -163,6 +165,31 @@ export function EditorFilters({ query, showing, embedded }: EditorFiltersProps):
component: LifecycleToggles as (props: EditorFilterProps) => JSX.Element | null,
}
: null,
isStickiness
? {
key: 'stickinessCriteria',
label: () => (
<div className="flex">
<span>Stickiness Criteria</span>
<Tooltip
closeDelayMs={200}
title={
<div className="space-y-2">
<div>
The stickiness criteria defines how many times a user must perform an
event inside of a given interval in order to be considered "sticky."
</div>
</div>
}
>
<IconInfo className="text-xl text-muted-alt shrink-0 ml-1" />
</Tooltip>
</div>
),
position: 'right',
component: StickinessCriteria as (props: EditorFilterProps) => JSX.Element | null,
}
: null,
{
key: 'properties',
label: 'Filters',
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/queries/nodes/InsightViz/StickinessCriteria.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { LemonInput } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { OperatorSelect } from 'lib/components/PropertyFilters/components/OperatorValueSelect'
import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic'

import { StickinessOperator } from '~/queries/schema'
import { EditorFilterProps, PropertyOperator } from '~/types'

export function StickinessCriteria({ insightProps }: EditorFilterProps): JSX.Element {
const { stickinessFilter } = useValues(insightVizDataLogic(insightProps))
const { updateInsightFilter } = useActions(insightVizDataLogic(insightProps))

const stickinessCriteria = stickinessFilter?.stickinessCriteria
const currentOperator = stickinessCriteria?.operator ?? PropertyOperator.GreaterThanOrEqual
const currentValue = stickinessCriteria?.value ?? 1

const operators: StickinessOperator[] = [
PropertyOperator.LessThanOrEqual,
PropertyOperator.GreaterThanOrEqual,
PropertyOperator.Exact,
]

return (
<div className="flex items-center gap-2">
<OperatorSelect
className="flex-1"
operator={currentOperator}
operators={operators}
onChange={(newOperator: PropertyOperator) => {
updateInsightFilter({
stickinessCriteria: { operator: newOperator as StickinessOperator, value: currentValue },
})
}}
/>
<LemonInput
type="number"
className="ml-2 w-20"
defaultValue={currentValue}
min={1}
onChange={(newValue: number | undefined) => {
if (newValue !== undefined) {
updateInsightFilter({ stickinessCriteria: { operator: currentOperator, value: newValue } })
}
}}
/>
time(s) per interval
</div>
)
}
17 changes: 17 additions & 0 deletions frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12226,6 +12226,19 @@
},
"showValuesOnSeries": {
"type": "boolean"
},
"stickinessCriteria": {
"additionalProperties": false,
"properties": {
"operator": {
"$ref": "#/definitions/StickinessOperator"
},
"value": {
"type": "integer"
}
},
"required": ["operator", "value"],
"type": "object"
}
},
"type": "object"
Expand Down Expand Up @@ -12265,6 +12278,10 @@
},
"type": "object"
},
"StickinessOperator": {
"enum": ["gte", "lte", "exact"],
"type": "string"
},
"StickinessQuery": {
"additionalProperties": false,
"properties": {
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/queries/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1500,11 +1500,20 @@ export interface PathsQuery extends InsightsQueryBase<PathsQueryResponse> {
/** `StickinessFilterType` minus everything inherited from `FilterType` and persons modal related params */
export type StickinessFilterLegacy = Omit<StickinessFilterType, keyof FilterType | 'stickiness_days' | 'shown_as'>

export type StickinessOperator =
| PropertyOperator.GreaterThanOrEqual
| PropertyOperator.LessThanOrEqual
| PropertyOperator.Exact

export type StickinessFilter = {
display?: StickinessFilterLegacy['display']
showLegend?: StickinessFilterLegacy['show_legend']
showValuesOnSeries?: StickinessFilterLegacy['show_values_on_series']
hiddenLegendIndexes?: integer[]
stickinessCriteria?: {
operator: StickinessOperator
value: integer
}
}

export const STICKINESS_FILTER_PROPERTIES = new Set<keyof StickinessFilter>([
Expand Down
92 changes: 56 additions & 36 deletions posthog/hogql_queries/insights/stickiness_query_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from posthog.hogql_queries.utils.query_previous_period_date_range import QueryPreviousPeriodDateRange
from posthog.models import Team
from posthog.models.action.action import Action
from posthog.models.cohort.util import get_count_operator, get_count_operator_ast
from posthog.models.filters.mixins.utils import cached_property
from posthog.schema import (
ActionsNode,
Expand Down Expand Up @@ -87,46 +88,63 @@ def _aggregation_expressions(self, series: EventsNode | ActionsNode | DataWareho

return ast.Field(chain=["e", "person_id"])

def _having_clause(self) -> ast.Expr:
if not (self.query.stickinessFilter and self.query.stickinessFilter.stickinessCriteria):
return parse_expr("count() > 0")
operator = self.query.stickinessFilter.stickinessCriteria.operator
value = ast.Constant(value=self.query.stickinessFilter.stickinessCriteria.value)
return parse_expr(f"""count() {get_count_operator(operator)} {{value}}""", {"value": value})

def _events_query(self, series_with_extra: SeriesWithExtras) -> ast.SelectQuery:
num_intervals_column_expr = ast.Alias(
alias="num_intervals",
expr=ast.Call(
distinct=True,
name="count",
args=[self.query_date_range.date_to_start_of_interval_hogql(ast.Field(chain=["e", "timestamp"]))],
),
inner_query = parse_select(
"""
SELECT
{aggregation} as aggregation_target,
{start_of_interval} as start_of_interval,
FROM events e
SAMPLE {sample}
WHERE {where_clause}
GROUP BY aggregation_target, start_of_interval
HAVING {having_clause}
""",
{
"aggregation": self._aggregation_expressions(series_with_extra.series),
"start_of_interval": self.query_date_range.date_to_start_of_interval_hogql(
ast.Field(chain=["e", "timestamp"])
),
"sample": self._sample_value(),
"where_clause": self.where_clause(series_with_extra),
"having_clause": self._having_clause(),
},
)

aggregation = ast.Alias(
alias="aggregation_target", expr=self._aggregation_expressions(series_with_extra.series)
middle_query = parse_select(
"""
SELECT
aggregation_target,
count() as num_intervals
FROM
{inner_query}
GROUP BY
aggregation_target
""",
{"inner_query": inner_query},
)

select_query = parse_select(
outer_query = parse_select(
"""
SELECT
count(DISTINCT aggregation_target),
num_intervals
FROM (
SELECT {aggregation}, {num_intervals_column_expr}
FROM events e
SAMPLE {sample}
WHERE {where_clause}
GROUP BY aggregation_target
)
WHERE num_intervals <= {num_intervals}
GROUP BY num_intervals
ORDER BY num_intervals
SELECT
count(DISTINCT aggregation_target) as num_actors,
num_intervals
FROM
{middle_query}
GROUP BY num_intervals
ORDER BY num_intervals
""",
placeholders={
"where_clause": self.where_clause(series_with_extra),
"num_intervals": ast.Constant(value=self.intervals_num()),
"sample": self._sample_value(),
"num_intervals_column_expr": num_intervals_column_expr,
"aggregation": aggregation,
},
{"middle_query": middle_query},
)

return cast(ast.SelectQuery, select_query)
return cast(ast.SelectQuery, outer_query)

def to_query(self) -> ast.SelectSetQuery:
return ast.SelectSetQuery.create_from_queries(self.to_queries(), "UNION ALL")
Expand All @@ -145,12 +163,12 @@ def to_queries(self) -> list[ast.SelectQuery]:
select_query = parse_select(
"""
SELECT
groupArray(aggregation_target) as counts,
groupArray(num_actors) as counts,
groupArray(num_intervals) as intervals
FROM (
SELECT sum(aggregation_target) as aggregation_target, num_intervals
SELECT sum(num_actors) as num_actors, num_intervals
FROM (
SELECT 0 as aggregation_target, (number + 1) as num_intervals
SELECT 0 as num_actors, (number + 1) as num_intervals
FROM numbers(dateDiff({interval}, {date_from_start_of_interval}, {date_to_start_of_interval} + {interval_addition}))
UNION ALL
{events_query}
Expand All @@ -170,7 +188,9 @@ def to_queries(self) -> list[ast.SelectQuery]:

return queries

def to_actors_query(self, interval_num: Optional[int] = None) -> ast.SelectQuery | ast.SelectSetQuery:
def to_actors_query(
self, interval_num: Optional[int] = None, operator: Optional[str] = None
) -> ast.SelectQuery | ast.SelectSetQuery:
queries: list[ast.SelectQuery] = []

for series in self.series:
Expand All @@ -188,7 +208,7 @@ def to_actors_query(self, interval_num: Optional[int] = None) -> ast.SelectQuery
if interval_num is not None:
events_query.where = ast.CompareOperation(
left=ast.Field(chain=["num_intervals"]),
op=ast.CompareOperationOp.Eq,
op=ast.CompareOperationOp.Eq if operator is None else get_count_operator_ast(operator),
right=ast.Constant(value=interval_num),
)

Expand Down
Loading

0 comments on commit b71ae75

Please sign in to comment.