defmodule Plausible.FunnelsTest do use Plausible.DataCase @moduletag :ee_only on_ee do alias Plausible.Goals alias Plausible.Funnels alias Plausible.Stats alias Plausible.Stats.QueryBuilder setup do site = new_site() {:ok, g1} = Goals.create(site, %{"page_path" => "/go/to/blog/**"}) {:ok, g2} = Goals.create(site, %{"event_name" => "Signup"}) {:ok, g3} = Goals.create(site, %{"page_path" => "/checkout"}) {:ok, g4} = Goals.create(site, %{"event_name" => "Leave feedback"}) {:ok, g5} = Goals.create(site, %{"page_path" => "/recommend"}) {:ok, g6} = Goals.create(site, %{"event_name" => "Extra event"}) {:ok, g7} = Goals.create(site, %{"event_name" => "Extra event 2"}) {:ok, g8} = Goals.create(site, %{"event_name" => "Extra event 3"}) {:ok, g9} = Goals.create(site, %{"event_name" => "Extra event 4"}) {:ok, %{ site: site, goals: [g1, g2, g3], steps: [g1, g2, g3, g4, g5, g6, g7, g8, g9] |> Enum.map(&%{"goal_id" => &1.id}) }} end describe "Common context" do test "create and store a funnel given a set of goals", %{ site: site, steps: [g1, g2, g3 | _] } do {:ok, funnel} = Funnels.create( site, "From blog to signup and purchase", [g1, g2, g3] ) assert funnel.inserted_at assert funnel.name == "From blog to signup and purchase" assert funnel.strict_order == false assert [fg1, fg2, fg3] = funnel.steps assert fg1.goal_id == g1["goal_id"] assert fg2.goal_id == g2["goal_id"] assert fg3.goal_id == g3["goal_id"] assert fg1.step_order == 1 assert fg2.step_order == 2 assert fg3.step_order == 3 end test "update funnel", %{site: site, steps: [g1, g2, g3, g4, g5 | _]} do {:ok, funnel1} = Funnels.create( site, "Sample funnel", [g1, g2, g3] ) {:ok, funnel2} = Funnels.update(funnel1, "Updated funnel", [g4, g5]) assert funnel2.name == "Updated funnel" assert [fg1, fg2] = funnel2.steps assert fg1.goal_id == g4["goal_id"] assert fg2.goal_id == g5["goal_id"] assert fg1.step_order == 1 assert fg2.step_order == 2 assert funnel1.id == funnel2.id end test "update funnel strict_order", %{site: site, steps: [g1, g2, g3 | _]} do {:ok, funnel} = Funnels.create( site, "Sample funnel", [g1, g2] ) assert funnel.strict_order == false {:ok, strict_funnel} = Funnels.update( funnel, "Sample funnel", [g1, g2, g3], strict_order?: true ) assert strict_funnel.strict_order == true {:ok, preserved_funnel} = Funnels.update( strict_funnel, "Sample funnel", [g1, g2] ) assert preserved_funnel.strict_order == true end test "retrieve a funnel by id and site, get steps in order", %{ site: site, steps: [g1, g2, g3 | _] } do {:ok, funnel} = Funnels.create( site, "Lorem ipsum", [g3, g1, g2] ) assert funnel = Funnels.get(site, funnel.id) assert funnel.name == "Lorem ipsum" assert [%{step_order: 1}, %{step_order: 2}, %{step_order: 3}] = funnel.steps end test "funnels can be deleted by id and a site", %{site: site, steps: [g1, g2 | _]} do {:ok, funnel} = Funnels.create( site, "Sample funnel", [g1, g2] ) assert :ok = Funnels.delete(site, funnel.id) # deletion is idempotent assert :ok = Funnels.delete(site, funnel.id) refute Funnels.get(site, funnel.id) end test "a funnel cannot be made of < 2 steps", %{site: site, steps: [g1 | _]} do assert {:error, :invalid_funnel_size} = Funnels.create( site, "Lorem ipsum", [g1] ) end test "a funnel can be made of 8 steps maximum", %{site: site, steps: too_many_steps} do assert {:ok, _} = Funnels.create( site, "Lorem ipsum", Enum.take(too_many_steps, 8) ) assert {:error, :invalid_funnel_size} = Funnels.create( site, "Lorem ipsum", too_many_steps ) end test "a goal can only appear once in a funnel", %{steps: [g1 | _], site: site} do {:error, _changeset} = Funnels.create( site, "Lorem ipsum", [g1, g1] ) end test "funnels can be listed per site, starting from most recently added", %{ site: site, steps: [g1, g2, g3 | _] } do Funnels.create(site, "Funnel 1", [g3, g1]) Funnels.create(site, "Funnel 2", [g2, g1, g3]) funnels_list = Funnels.list(site) assert [%{name: "Funnel 2", steps_count: 3}, %{name: "Funnel 1", steps_count: 2}] = funnels_list end end describe "Plausible.Stats.Funnel" do test "funnels can be evaluated per site within a time range against an ephemeral definition", %{ site: site, goals: [g1, g2, g3 | _] } do funnel_definition = Funnels.ephemeral_definition( site, "From blog to signup and purchase", [ %{ "goal_id" => "#{g1.id}", "goal" => %{ "id" => "#{g1.id}", "event_name" => g1.event_name, "page_path" => g1.page_path } }, %{ "goal_id" => g2.id, "goal" => %{ "id" => g2.id, "event_name" => g2.event_name, "page_path" => g2.page_path } }, %{ "goal_id" => g3.id, "goal" => %{ "id" => g3.id, "event_name" => g3.event_name, "page_path" => g3.page_path } } ] ) populate_stats(site, [ build(:pageview, pathname: "/irrelevant/page/not/in/funnel", user_id: 999), build(:pageview, pathname: "/go/to/blog/foo", user_id: 123), build(:event, name: "Signup", user_id: 123), build(:pageview, pathname: "/checkout", user_id: 123), build(:pageview, pathname: "/go/to/blog/bar", user_id: 666), build(:event, name: "Signup", user_id: 666) ]) query = QueryBuilder.build!(site, input_date_range: :all) funnel_data = Stats.funnel(site, query, funnel_definition) assert {:ok, %{ all_visitors: 3, entering_visitors: 2, entering_visitors_percentage: "66.67", never_entering_visitors: 1, never_entering_visitors_percentage: "33.33", steps: [ %{ label: "Visit /go/to/blog/**", visitors: 2, conversion_rate: "100", conversion_rate_step: "0", dropoff: 0, dropoff_percentage: "0" }, %{ label: "Signup", visitors: 2, conversion_rate: "100", conversion_rate_step: "100", dropoff: 0, dropoff_percentage: "0" }, %{ label: "Visit /checkout", visitors: 1, conversion_rate: "50", conversion_rate_step: "50", dropoff: 1, dropoff_percentage: "50" } ] }} = funnel_data end test "funnels can be evaluated per site within a time range", %{ site: site, steps: [g1, g2, g3 | _] } do {:ok, funnel} = Funnels.create( site, "From blog to signup and purchase", [g1, g2, g3] ) populate_stats(site, [ build(:pageview, pathname: "/go/to/blog/foo", user_id: 123), build(:event, name: "Signup", user_id: 123), build(:pageview, pathname: "/checkout", user_id: 123), build(:pageview, pathname: "/go/to/blog/bar", user_id: 666), build(:event, name: "Signup", user_id: 666) ]) query = QueryBuilder.build!(site, input_date_range: :all) funnel_data = Stats.funnel(site, query, funnel.id) assert {:ok, %{ all_visitors: 2, entering_visitors: 2, entering_visitors_percentage: "100", never_entering_visitors: 0, never_entering_visitors_percentage: "0", steps: [ %{ label: "Visit /go/to/blog/**", visitors: 2, conversion_rate: "100", conversion_rate_step: "0", dropoff: 0 }, %{ label: "Signup", visitors: 2, conversion_rate: "100", conversion_rate_step: "100", dropoff: 0, dropoff_percentage: "0" }, %{ label: "Visit /checkout", visitors: 1, conversion_rate: "50", conversion_rate_step: "50", dropoff: 1, dropoff_percentage: "50" } ] }} = funnel_data end test "strict-order funnels stop at intervention events", %{ site: site, steps: [g1, g2, g3 | _] } do {:ok, non_strict_funnel} = Funnels.create( site, "From blog to signup and purchase", [g1, g2, g3] ) {:ok, strict_funnel} = Funnels.create( site, "Strict from blog to signup and purchase", [g1, g2, g3], strict_order?: true ) populate_stats(site, [ build(:pageview, pathname: "/go/to/blog/foo", user_id: 123, timestamp: ~N[2021-01-01 00:00:00] ), build(:pageview, pathname: "/some/other/page", user_id: 123, timestamp: ~N[2021-01-01 00:00:01] ), build(:event, name: "Signup", user_id: 123, timestamp: ~N[2021-01-01 00:00:02] ), build(:pageview, pathname: "/checkout", user_id: 123, timestamp: ~N[2021-01-01 00:00:03] ) ]) query = QueryBuilder.build!(site, input_date_range: :all) {:ok, non_strict_funnel_data} = Stats.funnel(site, query, non_strict_funnel.id) {:ok, strict_funnel_data} = Stats.funnel(site, query, strict_funnel.id) assert Enum.at(non_strict_funnel_data.steps, 2).visitors == 1 assert Enum.at(strict_funnel_data.steps, 2).visitors == 0 end test "funnels can be evaluated even where there are no visits yet", %{ site: site, steps: [g1, g2, g3 | _] } do {:ok, funnel} = Funnels.create( site, "From blog to signup and purchase", [g1, g2, g3] ) query = QueryBuilder.build!(site, input_date_range: :all) funnel_data = Stats.funnel(site, query, funnel.id) assert {:ok, %{ all_visitors: 0, entering_visitors: 0, entering_visitors_percentage: "0", never_entering_visitors: 0, never_entering_visitors_percentage: "0", steps: [ %{ label: "Visit /go/to/blog/**", 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 /checkout", visitors: 0, conversion_rate: "0", conversion_rate_step: "0", dropoff: 0, dropoff_percentage: "0" } ] }} = funnel_data end @tag :slow test "sampling", %{site: site, steps: [g1, g2, g3 | _]} do {:ok, funnel} = Funnels.create( site, "From blog to signup and purchase", [g1, g2, g3] ) stats = 1..50_000 |> Enum.flat_map(fn _ -> user_id = SipHash.hash!("0123456789ABCDEF", :crypto.strong_rand_bytes(64)) [ build(:pageview, pathname: "/go/to/blog/foo", user_id: user_id), build(:event, name: "Signup", user_id: user_id), build(:pageview, pathname: "/checkout", user_id: user_id) ] end) populate_stats(site, stats) query = Plausible.Stats.Query.from(site, %{"period" => "all", "sample_threshold" => "10000"}) {:ok, funnel_data} = Stats.funnel(site, query, funnel.id) assert_in_delta funnel_data[:all_visitors], 50_000, 5000 end end end end