Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(retention): filters on start/return event #27770

Merged
merged 11 commits into from
Jan 22, 2025
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.
14 changes: 9 additions & 5 deletions frontend/src/queries/nodes/InsightViz/EditorFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,15 @@ export function EditorFilters({ query, showing, embedded }: EditorFiltersProps):
{
title: 'General',
editorFilters: filterFalsy([
isRetention && {
key: 'retention-summary',
label: 'Retention Summary',
component: RetentionSummary,
},
...(isRetention
? [
{
key: 'retention-config',
label: 'Retention Summary',
component: RetentionSummary,
},
]
: []),
...(isPaths
? filterFalsy([
{
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/queries/nodes/InsightViz/InsightViz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils'
import { ErrorBoundary } from '~/layout/ErrorBoundary'
import { DashboardFilter, HogQLVariable, InsightVizNode } from '~/queries/schema'
import { QueryContext } from '~/queries/types'
import { isFunnelsQuery } from '~/queries/utils'
import { isFunnelsQuery, isRetentionQuery } from '~/queries/utils'
import { InsightLogicProps, ItemMode } from '~/types'

import { dataNodeLogic, DataNodeLogicProps } from '../DataNode/dataNodeLogic'
Expand Down Expand Up @@ -85,6 +85,7 @@ export function InsightViz({

const isFunnels = isFunnelsQuery(query.source)
const isHorizontalAlways = useFeatureFlag('INSIGHT_HORIZONTAL_CONTROLS')
const isRetention = isRetentionQuery(query.source)

const showIfFull = !!query.full
const disableHeader = embedded || !(query.showHeader ?? showIfFull)
Expand Down Expand Up @@ -120,7 +121,7 @@ export function InsightViz({
className={
!isEmbedded
? clsx('InsightViz', {
'InsightViz--horizontal': isFunnels || isHorizontalAlways,
'InsightViz--horizontal': isFunnels || isRetention || isHorizontalAlways,
})
: 'InsightCard__viz'
}
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12361,6 +12361,13 @@
"order": {
"type": "integer"
},
"properties": {
"description": "filters on the event",
"items": {
"$ref": "#/definitions/AnyPropertyFilter"
},
"type": "array"
},
"type": {
"$ref": "#/definitions/EntityType"
},
Expand Down
128 changes: 57 additions & 71 deletions frontend/src/scenes/insights/EditorFilters/RetentionSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,58 +27,70 @@ export function RetentionSummary({ insightProps }: EditorFilterProps): JSX.Eleme
const { targetEntity, returningEntity, retentionType, totalIntervals, period } = retentionFilter || {}

return (
<div className="space-y-2" data-attr="retention-summary">
<div className="space-y-3" data-attr="retention-summary">
<div className="flex items-center">
Show
For
{showGroupsOptions ? (
<AggregationSelect className="mx-2" insightProps={insightProps} hogqlAvailable={false} />
) : (
<b> Unique users </b>
)}
who performed
</div>
<div className="flex items-center">
event or action
<span className="mx-2">
<ActionFilter
entitiesLimit={1}
mathAvailability={MathAvailability.None}
hideFilter
hideRename
buttonCopy="Add graph series"
filters={{ events: [targetEntity] } as FilterType} // retention filters use target and returning entity instead of events
setFilters={(newFilters: FilterType) => {
if (newFilters.events && newFilters.events.length > 0) {
updateInsightFilter({ targetEntity: newFilters.events[0] })
} else if (newFilters.actions && newFilters.actions.length > 0) {
updateInsightFilter({ targetEntity: newFilters.actions[0] })
} else {
updateInsightFilter({ targetEntity: undefined })
}
}}
typeKey={`${keyForInsightLogicProps('new')(insightProps)}-targetEntity`}
/>
</span>
<LemonSelect
options={Object.entries(retentionOptions).map(([key, value]) => ({
label: value,
value: key,
element: (
<>
{value}
<Tooltip placement="right" title={retentionOptionDescriptions[key]}>
<IconInfo className="info-indicator" />
</Tooltip>
</>
),
}))}
value={retentionType ? retentionOptions[retentionType] : undefined}
onChange={(value): void => updateInsightFilter({ retentionType: value as RetentionType })}
dropdownMatchSelectWidth={false}
/>
</div>
<div className="flex items-center">
in the last
<div>who performed</div>
<ActionFilter
entitiesLimit={1}
mathAvailability={MathAvailability.None}
hideRename
filters={{ events: [targetEntity] } as FilterType} // retention filters use target and returning entity instead of events
setFilters={(newFilters: FilterType) => {
if (newFilters.events && newFilters.events.length > 0) {
updateInsightFilter({ targetEntity: newFilters.events[0] })
} else if (newFilters.actions && newFilters.actions.length > 0) {
updateInsightFilter({ targetEntity: newFilters.actions[0] })
} else {
updateInsightFilter({ targetEntity: undefined })
}
}}
typeKey={`${keyForInsightLogicProps('new')(insightProps)}-targetEntity`}
/>
<LemonSelect
options={Object.entries(retentionOptions).map(([key, value]) => ({
label: value,
value: key,
element: (
<>
{value}
<Tooltip placement="right" title={retentionOptionDescriptions[key]}>
<IconInfo className="info-indicator" />
</Tooltip>
</>
),
}))}
value={retentionType ? retentionOptions[retentionType] : undefined}
onChange={(value): void => updateInsightFilter({ retentionType: value as RetentionType })}
dropdownMatchSelectWidth={false}
/>

<div>and then returned to perform</div>
<ActionFilter
entitiesLimit={1}
mathAvailability={MathAvailability.None}
hideRename
buttonCopy="Add graph series"
filters={{ events: [returningEntity] } as FilterType}
setFilters={(newFilters: FilterType) => {
if (newFilters.events && newFilters.events.length > 0) {
updateInsightFilter({ returningEntity: newFilters.events[0] })
} else if (newFilters.actions && newFilters.actions.length > 0) {
updateInsightFilter({ returningEntity: newFilters.actions[0] })
} else {
updateInsightFilter({ returningEntity: undefined })
}
}}
typeKey={`${keyForInsightLogicProps('new')(insightProps)}-returningEntity`}
/>
<div className="flex items-center gap-2">
<div>on any of the next</div>
<LemonInput
type="number"
className="ml-2 w-20"
Expand All @@ -104,7 +116,6 @@ export function RetentionSummary({ insightProps }: EditorFilterProps): JSX.Eleme
}}
/>
<LemonSelect
className="mx-2"
value={period}
onChange={(value): void => updateInsightFilter({ period: value ? value : undefined })}
options={dateOptions.map((period) => ({
Expand All @@ -113,31 +124,6 @@ export function RetentionSummary({ insightProps }: EditorFilterProps): JSX.Eleme
}))}
dropdownMatchSelectWidth={false}
/>
and then came back to perform
</div>
<div className="flex items-center">
event or action
<span className="mx-2">
<ActionFilter
entitiesLimit={1}
mathAvailability={MathAvailability.None}
hideFilter
hideRename
buttonCopy="Add graph series"
filters={{ events: [returningEntity] } as FilterType}
setFilters={(newFilters: FilterType) => {
if (newFilters.events && newFilters.events.length > 0) {
updateInsightFilter({ returningEntity: newFilters.events[0] })
} else if (newFilters.actions && newFilters.actions.length > 0) {
updateInsightFilter({ returningEntity: newFilters.actions[0] })
} else {
updateInsightFilter({ returningEntity: undefined })
}
}}
typeKey={`${keyForInsightLogicProps('new')(insightProps)}-returningEntity`}
/>
</span>
on any of the next {dateOptionPlurals[period ?? 'Day']}.
</div>
<div>
<p className="text-muted mt-4">
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2401,6 +2401,8 @@ export interface RetentionEntity {
order?: number
uuid?: string
custom_name?: string
/** filters on the event */
properties?: AnyPropertyFilter[]
}

export interface RetentionFilterType extends FilterType {
Expand Down
29 changes: 19 additions & 10 deletions posthog/hogql/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,10 +509,12 @@ def property_to_expr(
cohort = Cohort.objects.get(team__project_id=team.project_id, id=property.value)
return ast.CompareOperation(
left=ast.Field(chain=["id" if scope == "person" else "person_id"]),
op=ast.CompareOperationOp.NotInCohort
# Kludge: negation is outdated but still used in places
if property.negation or property.operator == PropertyOperator.NOT_IN.value
else ast.CompareOperationOp.InCohort,
op=(
ast.CompareOperationOp.NotInCohort
# Kludge: negation is outdated but still used in places
if property.negation or property.operator == PropertyOperator.NOT_IN.value
else ast.CompareOperationOp.InCohort
),
right=ast.Constant(value=cohort.pk),
)

Expand Down Expand Up @@ -622,18 +624,25 @@ def action_to_expr(action: Action) -> ast.Expr:
return ast.Or(exprs=or_queries)


def entity_to_expr(entity: RetentionEntity) -> ast.Expr:
def entity_to_expr(entity: RetentionEntity, team: Team) -> ast.Expr:
if entity.type == TREND_FILTER_TYPE_ACTIONS and entity.id is not None:
action = Action.objects.get(pk=entity.id)
return action_to_expr(action)
if entity.id is None:
return ast.Constant(value=True)

return ast.CompareOperation(
op=ast.CompareOperationOp.Eq,
left=ast.Field(chain=["events", "event"]),
right=ast.Constant(value=entity.id),
)
filters: list[ast.Expr] = [
ast.CompareOperation(
op=ast.CompareOperationOp.Eq,
left=ast.Field(chain=["events", "event"]),
right=ast.Constant(value=entity.id),
)
]

if entity.properties is not None and entity.properties != []:
filters.append(property_to_expr(entity.properties, team))

return ast.And(exprs=filters)


def tag_name_to_expr(tag_name: str):
Expand Down
Loading
Loading