diff --git a/extra/lib/plausible/stats/funnel.ex b/extra/lib/plausible/stats/funnel.ex index a9fae4ca90d6..46ee57b1676d 100644 --- a/extra/lib/plausible/stats/funnel.ex +++ b/extra/lib/plausible/stats/funnel.ex @@ -28,6 +28,10 @@ defmodule Plausible.Stats.Funnel do end def funnel(_site, query, %Funnel{} = funnel) do + funnel_goals = Enum.map(funnel.steps, & &1.goal) + + query = %{query | preloaded_goals: %{all: funnel_goals}} + funnel_data = query |> Base.base_event_query() diff --git a/lib/plausible/stats/sql/where_builder.ex b/lib/plausible/stats/sql/where_builder.ex index 18a15546c0b1..9cf057c1d49a 100644 --- a/lib/plausible/stats/sql/where_builder.ex +++ b/lib/plausible/stats/sql/where_builder.ex @@ -19,6 +19,23 @@ defmodule Plausible.Stats.SQL.WhereBuilder do ] @doc "Builds WHERE clause for a given Query against sessions or events table" + def build(:events, query) do + base_condition = filter_site_time_range(:events, query) + + engagement_condition = + if skip_engagement_events?(query) do + dynamic([e], e.name != "engagement") + else + true + end + + query.filters + |> Enum.map(&add_filter(:events, query, &1)) + |> Enum.reduce(dynamic([], ^base_condition and ^engagement_condition), fn condition, acc -> + dynamic([], ^acc and ^condition) + end) + end + def build(table, query) do base_condition = filter_site_time_range(table, query) @@ -27,6 +44,23 @@ defmodule Plausible.Stats.SQL.WhereBuilder do |> Enum.reduce(base_condition, fn condition, acc -> dynamic([], ^acc and ^condition) end) end + # Engagement events exist solely for time_on_page and scroll_depth tracking. + # Because 'name' is part of the events_v2 primary key, adding WHERE name != 'engagement' + # lets ClickHouse skip entire index granules with only engagement rows. + # This is safe whenever none of the requested metrics or goal filters need engagement rows. + defp skip_engagement_events?(query) do + :time_on_page not in query.metrics and + :scroll_depth not in query.metrics and + not scroll_goal_involved?(query) + end + + defp scroll_goal_involved?(%{preloaded_goals: %{all: goals}} = query) do + Enum.any?(goals, fn goal -> Plausible.Goal.type(goal) == :scroll end) or + "event:goal" in query.dimensions + end + + defp scroll_goal_involved?(_query), do: false + @doc """ Builds WHERE clause condition based off of a filter and a custom column name Used for special business logic cases