Skip to content

Commit

Permalink
feat: add first matching event for trends math (#26774)
Browse files Browse the repository at this point in the history
Co-authored-by: Peter Kirkham <[email protected]>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sandy Spicer <[email protected]>
  • Loading branch information
4 people authored Dec 10, 2024
1 parent 11f5857 commit 49f8a68
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 11 deletions.
10 changes: 9 additions & 1 deletion frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1470,7 +1470,15 @@
"type": "string"
},
"BaseMathType": {
"enum": ["total", "dau", "weekly_active", "monthly_active", "unique_session", "first_time_for_user"],
"enum": [
"total",
"dau",
"weekly_active",
"monthly_active",
"unique_session",
"first_time_for_user",
"first_matching_event_for_user"
],
"type": "string"
},
"BinCountValue": {
Expand Down
23 changes: 20 additions & 3 deletions frontend/src/scenes/trends/mathsLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,29 @@ export const BASE_MATH_DEFINITIONS: Record<BaseMathType, MathDefinition> = {
shortName: 'first time',
description: (
<>
Only count events if users do it for the first time.
Only the first time the user performed this event will count, and only if it matches the event filters.
<br />
<br />
<i>
Example: If a single user performs an event for the first time ever within a given period, it counts
as 1. Subsequent events by the same user will not be counted.
Example: If the we are looking for pageview events to posthog.com/about, but the user's first
pageview was on posthog.com, it will not match, even if they went to posthog.com/about afterwards.
</i>
</>
),
category: MathCategory.EventCount,
},
[BaseMathType.FirstMatchingEventForUser]: {
name: 'First matching event for user',
shortName: 'first matching event',
description: (
<>
The first time the user performed this event that matches the event filters will count.
<br />
<br />
<i>
Example: If the we are looking for pageview events to posthog.com/about, and the user's first
pageview was on posthog.com but then they navigated to posthog.com/about, it will match the pageview
event from posthog.com/about
</i>
</>
),
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3614,6 +3614,7 @@ export enum BaseMathType {
MonthlyActiveUsers = 'monthly_active',
UniqueSessions = 'unique_session',
FirstTimeForUser = 'first_time_for_user',
FirstMatchingEventForUser = 'first_matching_event_for_user',
}

export enum PropertyMathType {
Expand Down
11 changes: 10 additions & 1 deletion posthog/hogql_queries/insights/trends/aggregation_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def requires_query_orchestration(self) -> bool:
"weekly_active",
"monthly_active",
"first_time_for_user",
"first_matching_event_for_user",
]

return self.is_count_per_actor_variant() or self.series.math in math_to_return_true
Expand All @@ -116,6 +117,9 @@ def is_active_users_math(self):
def is_first_time_ever_math(self):
return self.series.math == "first_time_for_user"

def is_first_matching_event(self):
return self.series.math == "first_matching_event_for_user"

def _math_func(self, method: str, override_chain: Optional[list[str | int]]) -> ast.Call:
if override_chain is not None:
return ast.Call(name=method, args=[ast.Field(chain=override_chain)])
Expand Down Expand Up @@ -452,7 +456,11 @@ def _first_time_parent_query(self, inner_query: ast.SelectQuery):
return query

def get_first_time_math_query_orchestrator(
self, events_where_clause: ast.Expr, sample_value: ast.RatioExpr, event_name_filter: ast.Expr | None = None
self,
events_where_clause: ast.Expr,
sample_value: ast.RatioExpr,
event_name_filter: ast.Expr | None = None,
is_first_matching_event: bool = False,
):
date_placeholders = self.query_date_range.to_placeholders()
date_from = parse_expr(
Expand All @@ -479,6 +487,7 @@ def __init__(self):
filters=events_where_clause,
event_or_action_filter=event_name_filter,
ratio=sample_value,
is_first_matching_event=is_first_matching_event,
)
self.parent_query_builder = QueryAlternator(parent_select)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5013,3 +5013,57 @@ def test_trends_aggregation_total_with_null(self):

assert len(response.results) == 1
assert response.results[0]["data"] == [1.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.1]

def test_trends_aggregation_first_matching_event_for_user(self):
_create_person(
team=self.team,
distinct_ids=["p1"],
properties={},
)
_create_event(
team=self.team,
event="$pageview",
distinct_id="p1",
timestamp="2020-01-08T12:00:00Z",
properties={"$browser": "Chrome"},
)
_create_event(
team=self.team,
event="$pageview",
distinct_id="p1",
timestamp="2020-01-09T12:00:00Z",
properties={"$browser": "Chrome"},
)
_create_event(
team=self.team,
event="$pageview",
distinct_id="p1",
timestamp="2020-01-10T12:00:00Z",
properties={"$browser": "Firefox"},
)
_create_event(
team=self.team,
event="$pageview",
distinct_id="p1",
timestamp="2020-01-11T12:00:00Z",
properties={"$browser": "Firefox"},
)
flush_persons_and_events()

response = self._run_trends_query(
"2020-01-08",
"2020-01-11",
IntervalType.DAY,
[
EventsNode(
event="$pageview",
math=BaseMathType.FIRST_MATCHING_EVENT_FOR_USER,
properties=[EventPropertyFilter(key="$browser", operator=PropertyOperator.EXACT, value="Firefox")],
)
],
TrendsFilter(display=ChartDisplayType.ACTIONS_LINE_GRAPH),
)

assert len(response.results) == 1
assert response.results[0]["count"] == 1
assert response.results[0]["data"] == [0, 0, 1, 0]
7 changes: 4 additions & 3 deletions posthog/hogql_queries/insights/trends/trends_query_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,15 @@ def _get_events_subquery(

return wrapper
# Just complex series aggregation
elif (
self._aggregation_operation.requires_query_orchestration()
and self._aggregation_operation.is_first_time_ever_math()
elif self._aggregation_operation.requires_query_orchestration() and (
self._aggregation_operation.is_first_time_ever_math()
or self._aggregation_operation.is_first_matching_event()
):
return self._aggregation_operation.get_first_time_math_query_orchestrator(
events_where_clause=events_filter,
sample_value=self._sample_value(),
event_name_filter=self._event_or_action_where_expr(),
is_first_matching_event=self._aggregation_operation.is_first_matching_event(),
).build()
elif self._aggregation_operation.requires_query_orchestration():
return self._aggregation_operation.get_actors_query_orchestrator(
Expand Down
12 changes: 9 additions & 3 deletions posthog/hogql_queries/insights/utils/aggregations.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,26 @@ def __init__(
filters: ast.Expr | None = None,
event_or_action_filter: ast.Expr | None = None,
ratio: ast.RatioExpr | None = None,
is_first_matching_event: bool = False,
):
query.select = self._select_expr(date_from, filters)
query.select = self._select_expr(date_from, filters, is_first_matching_event)
query.select_from = self._select_from_expr(ratio)
query.where = self._where_expr(date_to, event_or_action_filter)
query.group_by = self._group_by_expr()
query.having = self._having_expr()
super().__init__(query)

def _select_expr(self, date_from: ast.Expr, filters: ast.Expr | None = None):
def _select_expr(self, date_from: ast.Expr, filters: ast.Expr | None = None, is_first_matching_event: bool = False):
aggregation_filters = date_from if filters is None else ast.And(exprs=[date_from, filters])
min_timestamp_expr = (
ast.Call(name="min", args=[ast.Field(chain=["timestamp"])])
if not is_first_matching_event or filters is None
else ast.Call(name="minIf", args=[ast.Field(chain=["timestamp"]), filters])
)
return [
ast.Alias(
alias="min_timestamp",
expr=ast.Call(name="min", args=[ast.Field(chain=["timestamp"])]),
expr=min_timestamp_expr,
),
ast.Alias(
alias="min_timestamp_with_condition",
Expand Down
1 change: 1 addition & 0 deletions posthog/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ class BaseMathType(StrEnum):
MONTHLY_ACTIVE = "monthly_active"
UNIQUE_SESSION = "unique_session"
FIRST_TIME_FOR_USER = "first_time_for_user"
FIRST_MATCHING_EVENT_FOR_USER = "first_matching_event_for_user"


class BreakdownAttributionType(StrEnum):
Expand Down

0 comments on commit 49f8a68

Please sign in to comment.