diff --git a/extra/lib/plausible/stats/funnel.ex b/extra/lib/plausible/stats/funnel.ex index a365f3610714..409f226c2ac9 100644 --- a/extra/lib/plausible/stats/funnel.ex +++ b/extra/lib/plausible/stats/funnel.ex @@ -80,7 +80,7 @@ defmodule Plausible.Stats.Funnel do defp select_funnel(db_query, funnel_definition) do window_funnel_steps = Enum.reduce(funnel_definition.steps, nil, fn step, acc -> - goal_condition = goal_condition(step.goal) + goal_condition = Plausible.Stats.Goals.goal_condition(step.goal) if acc do dynamic([q], fragment("?, ?", ^acc, ^goal_condition)) @@ -103,21 +103,6 @@ defmodule Plausible.Stats.Funnel do ) end - defp goal_condition(goal) do - case goal do - %Plausible.Goal{event_name: event} when is_binary(event) -> - dynamic([], fragment("name = ?", ^event)) - - %Plausible.Goal{page_path: pathname} when is_binary(pathname) -> - if String.contains?(pathname, "*") do - regex = Plausible.Stats.Filters.Utils.page_regex(pathname) - dynamic([], fragment("match(pathname, ?)", ^regex)) - else - dynamic([], fragment("pathname = ?", ^pathname)) - end - end - end - defp backfill_steps(funnel_result, funnel) do # Directly from ClickHouse we only get {step_idx(), visitor_count()} tuples. # but no totals including previous steps are aggregated. diff --git a/lib/plausible/stats/goals.ex b/lib/plausible/stats/goals.ex index 13660abf261d..0c6788569f7f 100644 --- a/lib/plausible/stats/goals.ex +++ b/lib/plausible/stats/goals.ex @@ -168,12 +168,16 @@ defmodule Plausible.Stats.Goals do if is_nil(goal) do dynamic([e], ^dynamic_statement) else - type = Plausible.Goal.type(goal) - dynamic([e], ^goal_condition(type, goal, imported?) or ^dynamic_statement) + dynamic([e], ^goal_condition(goal, imported?) or ^dynamic_statement) end end) end + def goal_condition(goal, imported? \\ false) do + type = Plausible.Goal.type(goal) + goal_condition(type, goal, imported?) + end + defp goal_condition(:event, goal, _) do dynamic([e], e.name == ^goal.event_name) end diff --git a/test/plausible_web/controllers/api/stats_controller/funnels_test.exs b/test/plausible_web/controllers/api/stats_controller/funnels_test.exs index 609511c93b84..ac45192f5833 100644 --- a/test/plausible_web/controllers/api/stats_controller/funnels_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/funnels_test.exs @@ -298,6 +298,120 @@ defmodule PlausibleWeb.Api.StatsController.FunnelsTest do end end + describe "GET /api/stats/funnel - page scroll goals" do + setup [:create_user, :log_in, :create_site] + + test "computes a funnel with page scroll goals", %{conn: conn, site: site} do + goals = [ + insert(:goal, site: site, event_name: "Onboarding Start"), + insert(:goal, + site: site, + page_path: "/onboard", + scroll_threshold: 25, + display_name: "Scroll 25% on /onboard" + ), + insert(:goal, + site: site, + page_path: "/onboard", + scroll_threshold: 50, + display_name: "Scroll 50% on /onboard" + ), + insert(:goal, + site: site, + page_path: "/onboard", + scroll_threshold: 75, + display_name: "Scroll 75% on /onboard" + ), + insert(:goal, site: site, page_path: "/onboard-completed") + ] + + {:ok, funnel} = + Plausible.Funnels.create(site, "Onboarding", Enum.map(goals, &%{"goal_id" => &1.id})) + + populate_stats(site, [ + # user 1 - completes the whole funnel + build(:event, user_id: 1, name: "Onboarding Start", timestamp: ~N[2021-01-01 00:00:00]), + build(:pageview, user_id: 1, pathname: "/onboard", timestamp: ~N[2021-01-01 00:00:10]), + build(:engagement, + user_id: 1, + pathname: "/onboard", + scroll_depth: 80, + timestamp: ~N[2021-01-01 00:00:20] + ), + build(:pageview, + user_id: 1, + pathname: "/onboard-completed", + timestamp: ~N[2021-01-01 00:00:30] + ), + # user 2 - drops off after scrolling 25% on /onboard + build(:event, user_id: 2, name: "Onboarding Start", timestamp: ~N[2021-01-01 00:00:00]), + build(:pageview, user_id: 2, pathname: "/onboard", timestamp: ~N[2021-01-01 00:00:10]), + build(:engagement, + user_id: 2, + pathname: "/onboard", + scroll_depth: 25, + timestamp: ~N[2021-01-01 00:00:20] + ) + ]) + + resp = + conn + |> get("/api/stats/#{site.domain}/funnels/#{funnel.id}/?period=day&date=2021-01-01") + |> json_response(200) + + assert %{ + "all_visitors" => 2, + "entering_visitors" => 2, + "entering_visitors_percentage" => "100", + "name" => "Onboarding", + "never_entering_visitors" => 0, + "never_entering_visitors_percentage" => "0", + "steps" => [ + %{ + "conversion_rate" => "100", + "conversion_rate_step" => "0", + "dropoff" => 0, + "dropoff_percentage" => "0", + "label" => "Onboarding Start", + "visitors" => 2 + }, + %{ + "conversion_rate" => "100", + "conversion_rate_step" => "100", + "dropoff" => 0, + "dropoff_percentage" => "0", + "label" => "Scroll 25% on /onboard", + "visitors" => 2 + }, + %{ + "conversion_rate" => "50", + "conversion_rate_step" => "50", + "dropoff" => 1, + "dropoff_percentage" => "50", + "label" => "Scroll 50% on /onboard", + "visitors" => 1 + }, + %{ + "conversion_rate" => "50", + "conversion_rate_step" => "100", + "dropoff" => 0, + "dropoff_percentage" => "0", + "label" => "Scroll 75% on /onboard", + "visitors" => 1 + }, + %{ + "conversion_rate" => "50", + "conversion_rate_step" => "100", + "dropoff" => 0, + "dropoff_percentage" => "0", + "label" => "Visit /onboard-completed", + "visitors" => 1 + } + ] + } = resp + end + end + defp setup_goals(site, goals) when is_list(goals) do goals = Enum.map(goals, fn {type, value} ->