defmodule PlausibleWeb.Api.StatsController.FunnelsTest do use PlausibleWeb.ConnCase, async: true @moduletag :ee_only on_ee do @user_id Enum.random(1000..9999) @other_user_id @user_id + 1 @build_funnel_with [ {"page_path", "/blog/announcement"}, {"event_name", "Signup"}, {"page_path", "/cart/add/product"}, {"event_name", "Purchase"} ] describe "GET /api/stats/funnel - default" do setup [:create_user, :log_in, :create_site] test "computes funnel for a day", %{conn: conn, site: site} do {:ok, funnel} = setup_funnel(site, @build_funnel_with) populate_stats(site, [ build(:pageview, pathname: "/some/irrelevant", user_id: 9_999_999), build(:pageview, pathname: "/blog/announcement", user_id: @user_id), build(:pageview, pathname: "/blog/announcement", user_id: @other_user_id), build(:event, name: "Signup", user_id: @user_id), build(:event, name: "Signup", user_id: @other_user_id), build(:pageview, pathname: "/cart/add/product", user_id: @user_id), build(:event, name: "Purchase", user_id: @user_id) ]) resp = conn |> get("/api/stats/#{site.domain}/funnels/#{funnel.id}/?period=day") |> json_response(200) assert %{ "name" => "Test funnel", "all_visitors" => 3, "entering_visitors" => 2, "entering_visitors_percentage" => "66.67", "never_entering_visitors" => 1, "never_entering_visitors_percentage" => "33.33", "steps" => [ %{ "conversion_rate" => "100", "conversion_rate_step" => "0", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Visit /blog/announcement", "visitors" => 2 }, %{ "conversion_rate" => "100", "conversion_rate_step" => "100", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Signup", "visitors" => 2 }, %{ "conversion_rate" => "50", "conversion_rate_step" => "50", "dropoff" => 1, "dropoff_percentage" => "50", "label" => "Visit /cart/add/product", "visitors" => 1 }, %{ "conversion_rate" => "50", "conversion_rate_step" => "100", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Purchase", "visitors" => 1 } ] } = resp end test "404 for unknown funnel", %{site: site, conn: conn} do resp = conn |> get("/api/stats/#{site.domain}/funnels/122873/?period=day") |> json_response(404) assert resp == %{"error" => "Funnel not found"} end test "400 for bad funnel ID", %{site: site, conn: conn} do resp = conn |> get("/api/stats/#{site.domain}/funnels/foobar/?period=day") |> json_response(400) assert resp == %{"error" => "There was an error with your request"} end test "computes all-time funnel with filters", %{conn: conn, user: user} do site = new_site(stats_start_date: ~D[2020-01-01], owner: user) {:ok, funnel} = setup_funnel(site, @build_funnel_with) populate_stats(site, [ build(:pageview, pathname: "/blog/announcement", user_id: @user_id), build(:pageview, pathname: "/blog/announcement", user_id: @other_user_id, timestamp: ~N[2021-01-01 12:00:00], utm_medium: "social" ), build(:event, name: "Signup", user_id: @user_id), build(:event, name: "Signup", user_id: @other_user_id, timestamp: ~N[2021-01-01 12:01:00], utm_medium: "social" ), build(:pageview, pathname: "/cart/add/product", user_id: @user_id), build(:event, name: "Purchase", user_id: @user_id) ]) filters = Jason.encode!([[:is, "visit:utm_medium", ["social"]]]) resp = conn |> get("/api/stats/#{site.domain}/funnels/#{funnel.id}/?period=all&filters=#{filters}") |> json_response(200) assert %{ "name" => "Test funnel", "all_visitors" => 1, "entering_visitors" => 1, "entering_visitors_percentage" => "100", "never_entering_visitors" => 0, "never_entering_visitors_percentage" => "0", "steps" => [ %{ "conversion_rate" => "100", "conversion_rate_step" => "0", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Visit /blog/announcement", "visitors" => 1 }, %{ "conversion_rate" => "100", "conversion_rate_step" => "100", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Signup", "visitors" => 1 }, %{ "conversion_rate" => "0", "conversion_rate_step" => "0", "dropoff" => 1, "dropoff_percentage" => "100", "label" => "Visit /cart/add/product", "visitors" => 0 }, %{ "conversion_rate" => "0", "conversion_rate_step" => "0", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Purchase", "visitors" => 0 } ] } = resp end test "computes an empty funnel", %{conn: conn, site: site} do {:ok, funnel} = setup_funnel(site, @build_funnel_with) resp = conn |> get("/api/stats/#{site.domain}/funnels/#{funnel.id}/?period=day") |> json_response(200) assert %{ "name" => "Test funnel", "all_visitors" => 0, "entering_visitors" => 0, "entering_visitors_percentage" => "0", "never_entering_visitors" => 0, "never_entering_visitors_percentage" => "0", "steps" => [ %{ "conversion_rate" => "0", "conversion_rate_step" => "0", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Visit /blog/announcement", "visitors" => 0 }, %{ "conversion_rate" => "0", "conversion_rate_step" => "0", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Signup", "visitors" => 0 }, %{ "conversion_rate" => "0", "conversion_rate_step" => "0", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Visit /cart/add/product", "visitors" => 0 }, %{ "conversion_rate" => "0", "conversion_rate_step" => "0", "dropoff" => 0, "dropoff_percentage" => "0", "label" => "Purchase", "visitors" => 0 } ] } = resp end test "returns HTTP 402 when site owner is on a growth plan", %{ conn: conn, user: user, site: site } do {:ok, funnel} = setup_funnel(site, @build_funnel_with) subscribe_to_growth_plan(user) resp = conn |> get("/api/stats/#{site.domain}/funnels/#{funnel.id}/?period=day") |> json_response(402) assert %{ "error" => "Funnels and user journeys is part of the Plausible Business plan. To get access to this feature, please upgrade your account." } == resp end end describe "GET /api/stats/funnel - disallowed filters" do setup [:create_user, :log_in, :create_site] test "event:page", %{conn: conn, site: site} do {:ok, funnel} = setup_funnel(site, @build_funnel_with) filters = Jason.encode!([[:is, "event:page", ["/pageA"]]]) resp = conn |> get("/api/stats/#{site.domain}/funnels/#{funnel.id}/?period=day&filters=#{filters}") |> json_response(400) assert resp == %{ "error" => "We are unable to show funnels when the dashboard is filtered by pages", "level" => "normal" } end test "event:goal", %{conn: conn, site: site} do {:ok, funnel} = setup_funnel(site, @build_funnel_with) filters = Jason.encode!([[:is, "event:goal", ["Signup"]], [:is, "event:page", ["/pageA"]]]) resp = conn |> get("/api/stats/#{site.domain}/funnels/#{funnel.id}/?period=day&filters=#{filters}") |> json_response(400) assert resp == %{ "error" => "We are unable to show funnels when the dashboard is filtered by goals", "level" => "normal" } end test "period: realtime", %{conn: conn, site: site} do {:ok, funnel} = setup_funnel(site, @build_funnel_with) resp = conn |> get("/api/stats/#{site.domain}/funnels/#{funnel.id}/?period=realtime") |> json_response(400) assert resp == %{ "error" => "We are unable to show funnels when the dashboard is filtered by realtime period", "level" => "normal" } 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} -> {:ok, g} = Plausible.Goals.create(site, %{type => value}) g end) {:ok, goals} end defp setup_funnel(site, goal_names) do {:ok, goals} = setup_goals(site, goal_names) Plausible.Funnels.create(site, "Test funnel", Enum.map(goals, &%{"goal_id" => &1.id})) end end end