diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 9079149cc2bfc..7c36c760576aa 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -250,8 +250,8 @@ export function renderColumn( const [parent, child] = key.split('.') return typeof record === 'object' ? record[parent][child] : 'unknown' } else { - if (typeof value === 'object' && value !== null) { - return 10 ? 0 : 1} /> + if (typeof value === 'object') { + return 10 ? 0 : 1} /> } return String(value) } diff --git a/frontend/src/scenes/retention/queries.ts b/frontend/src/scenes/retention/queries.ts index 7b8d738cc7109..f049c711582bb 100644 --- a/frontend/src/scenes/retention/queries.ts +++ b/frontend/src/scenes/retention/queries.ts @@ -5,10 +5,12 @@ import { ActorsQuery, NodeKind, RetentionQuery } from '~/queries/schema' export function retentionToActorsQuery(query: RetentionQuery, selectedInterval: number, offset = 0): ActorsQuery { const group = query.aggregation_group_type_index !== undefined - const select = group ? 'group' : 'person' + const selectActor = group ? 'group' : 'person' + const totalIntervals = (query.retentionFilter.total_intervals || 11) - selectedInterval + const selects = Array.from({ length: totalIntervals }, (_, intervalNumber) => `appearance_${intervalNumber}`) return { kind: NodeKind.ActorsQuery, - select: [select, 'appearances'], + select: [selectActor, ...selects], orderBy: ['length(appearances) DESC', 'actor_id'], source: { kind: NodeKind.InsightActorsQuery, @@ -25,13 +27,6 @@ export function retentionToActorsQuery(query: RetentionQuery, selectedInterval: } } -function appearances_1s_0s(appearances: number[], totalIntervals: number, selectedInterval: number | null): number[] { - const newTotalIntervals = totalIntervals - (selectedInterval ?? 0) - return Array.from({ length: newTotalIntervals }, (_, intervalNumber) => - appearances.includes(intervalNumber) ? 1 : 0 - ) -} - export async function queryForActors( retentionQuery: RetentionQuery, selectedInterval: number, @@ -41,7 +36,7 @@ export async function queryForActors( const response = await query(actorsQuery) const results: RetentionTableAppearanceType[] = response.results.map((row) => ({ person: row[0], - appearances: appearances_1s_0s(row[1], retentionQuery.retentionFilter.total_intervals || 11, selectedInterval), + appearances: row.slice(1, row.length), })) return { result: results, diff --git a/frontend/src/scenes/retention/retentionModalLogic.ts b/frontend/src/scenes/retention/retentionModalLogic.ts index ad605e13b1516..9aba029d17c2c 100644 --- a/frontend/src/scenes/retention/retentionModalLogic.ts +++ b/frontend/src/scenes/retention/retentionModalLogic.ts @@ -1,4 +1,6 @@ import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { retentionToActorsQuery } from 'scenes/retention/queries' @@ -19,7 +21,14 @@ export const retentionModalLogic = kea([ key(keyForInsightLogicProps(DEFAULT_RETENTION_LOGIC_KEY)), path((key) => ['scenes', 'retention', 'retentionModalLogic', key]), connect((props: InsightLogicProps) => ({ - values: [insightVizDataLogic(props), ['querySource'], groupsModel, ['aggregationLabel']], + values: [ + insightVizDataLogic(props), + ['querySource'], + groupsModel, + ['aggregationLabel'], + featureFlagLogic, + ['featureFlags'], + ], actions: [retentionPeopleLogic(props), ['loadPeople']], })), actions(() => ({ @@ -56,9 +65,9 @@ export const retentionModalLogic = kea([ }, ], exploreUrl: [ - (s) => [s.actorsQuery], - (actorsQuery): string | null => { - if (!actorsQuery) { + (s) => [s.actorsQuery, s.featureFlags], + (actorsQuery, featureFlags): string | null => { + if (!actorsQuery || !featureFlags?.[FEATURE_FLAGS.HOGQL_INSIGHTS_RETENTION]) { return null } const query: DataTableNode = { diff --git a/posthog/hogql_queries/insights/retention_query_runner.py b/posthog/hogql_queries/insights/retention_query_runner.py index 5484ba9c09a07..fec4d2b23dcd2 100644 --- a/posthog/hogql_queries/insights/retention_query_runner.py +++ b/posthog/hogql_queries/insights/retention_query_runner.py @@ -335,7 +335,8 @@ def to_actors_query(self, interval: Optional[int] = None) -> ast.SelectQuery: """ SELECT actor_id, - arraySort(groupArray(actor_activity.intervals_from_base)) AS appearances + groupArray(actor_activity.intervals_from_base) AS appearance_intervals, + arraySort(appearance_intervals) AS appearances FROM {actor_query} AS actor_activity @@ -346,4 +347,25 @@ def to_actors_query(self, interval: Optional[int] = None) -> ast.SelectQuery: }, timings=self.timings, ) + # We want to expose each interval as a separate column + for i in range(self.query_date_range.total_intervals - interval): + retention_query.select.append( + ast.Alias( + alias=f"appearance_{i}", + expr=ast.Call( + name="arrayExists", + args=[ + ast.Lambda( + args=["x"], + expr=ast.CompareOperation( + op=ast.CompareOperationOp.Eq, + left=ast.Field(chain=["x"]), + right=ast.Constant(value=i), + ), + ), + ast.Field(chain=["appearance_intervals"]), + ], + ), + ) + ) return retention_query diff --git a/posthog/hogql_queries/insights/test/test_retention_query_runner.py b/posthog/hogql_queries/insights/test/test_retention_query_runner.py index 999c93f57e71e..449cb15aa73e2 100644 --- a/posthog/hogql_queries/insights/test/test_retention_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_retention_query_runner.py @@ -70,14 +70,14 @@ def run_query(self, query): runner = RetentionQueryRunner(team=self.team, query=query) return runner.calculate().model_dump()["results"] - def run_actors_query(self, interval, query): + def run_actors_query(self, interval, query, select=None): query["kind"] = "RetentionQuery" if not query.get("retentionFilter"): query["retentionFilter"] = {} runner = ActorsQueryRunner( team=self.team, query={ - "select": ["person", "appearances"], + "select": ["person", "appearances", *(select or [])], "orderBy": ["length(appearances) DESC", "actor_id"], "source": { "kind": "InsightActorsQuery", @@ -752,6 +752,21 @@ def test_retention_people_basic(self): self.assertEqual(len(result), 1, result) self.assertEqual(result[0][0]["id"], person1.uuid, person1.uuid) + # test selecting appearances directly + result_2 = self.run_actors_query( + interval=0, + query={ + "dateRange": {"date_to": _date(10, hour=6)}, + }, + select=["appearance_0", "appearance_1", "appearance_2", "appearance_3", "appearance_4"], + ) + self.assertEqual(len(result_2), len(result)) + self.assertEqual(result_2[0][2], 1) # appearance_0 + self.assertEqual(result_2[0][3], 1) # appearance_1 + self.assertEqual(result_2[0][4], 1) # appearance_2 + self.assertEqual(result_2[0][5], 0) # appearance_3 + self.assertEqual(result_2[0][6], 0) # appearance_4 + def test_retention_people_first_time(self): _, _, p3, _ = self._create_first_time_retention_events() # even if set to hour 6 it should default to beginning of day and include all pageviews above