| 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, [ |
| |
| 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] |
| ), |
| |
| 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 |
|
|