Skip to content

Commit

Permalink
feat(hogql): Put retention actor appearances in columns (#19503)
Browse files Browse the repository at this point in the history
  • Loading branch information
webjunkie authored Jan 8, 2024
1 parent 729c5ca commit 8c1f2f5
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 19 deletions.
4 changes: 2 additions & 2 deletions frontend/src/queries/nodes/DataTable/renderColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <JSONViewer src={value} name={key} collapsed={Object.keys(value).length > 10 ? 0 : 1} />
if (typeof value === 'object') {
return <JSONViewer src={value} name={null} collapsed={Object.keys(value).length > 10 ? 0 : 1} />
}
return String(value)
}
Expand Down
15 changes: 5 additions & 10 deletions frontend/src/scenes/retention/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
17 changes: 13 additions & 4 deletions frontend/src/scenes/retention/retentionModalLogic.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -19,7 +21,14 @@ export const retentionModalLogic = kea<retentionModalLogicType>([
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(() => ({
Expand Down Expand Up @@ -56,9 +65,9 @@ export const retentionModalLogic = kea<retentionModalLogicType>([
},
],
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 = {
Expand Down
24 changes: 23 additions & 1 deletion posthog/hogql_queries/insights/retention_query_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
19 changes: 17 additions & 2 deletions posthog/hogql_queries/insights/test/test_retention_query_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8c1f2f5

Please sign in to comment.