From 896d78d8fd03dfdaab3bc1730e2c4d2776c280af Mon Sep 17 00:00:00 2001 From: Vini Brasil Date: Tue, 17 Oct 2023 10:00:00 -0300 Subject: [PATCH] Apply feature gates to dashboard queries (#3424) * Read feature status from Billing.Feature instead of %Site{} This commit changes data attributes passed to React. Previously the controller read feature statuses directly from the %Site{} schema. The Billing.Feature context is aware of the user plan and the features available. * Limit funnels internal API based on site owner plan * Limit props internal API based on site owner plan * Use site factory in QueryTest * Limit custom property filter based on site owner plan * Limit revenue goals queries based on site owner plan --- assets/js/dashboard/filters.js | 6 +- lib/plausible/stats/query.ex | 14 ++ lib/plausible/stats/util.ex | 14 +- .../controllers/api/stats_controller.ex | 30 ++++- .../controllers/stats_controller.ex | 15 ++- .../templates/stats/stats.html.eex | 6 +- test/plausible/debug_replay_info_test.exs | 2 +- test/plausible/stats/comparisons_test.exs | 34 ++--- test/plausible/stats/query_test.exs | 124 +++++++++++------- .../custom_prop_breakdown_test.exs | 12 ++ .../api/stats_controller/funnels_test.exs | 20 +++ .../api/stats_controller/top_stats_test.exs | 23 ++++ .../controllers/stats_controller_test.exs | 15 +++ 13 files changed, 230 insertions(+), 85 deletions(-) diff --git a/assets/js/dashboard/filters.js b/assets/js/dashboard/filters.js index 3c74744922..78ae6c6b25 100644 --- a/assets/js/dashboard/filters.js +++ b/assets/js/dashboard/filters.js @@ -94,8 +94,10 @@ function DropdownContent({ history, site, query, wrapped }) { const [addingFilter, setAddingFilter] = useState(false); if (wrapped === 0 || addingFilter) { - return Object.keys(FILTER_GROUPS) - .map((option) => filterDropdownOption(site, option)) + let filterGroups = {...FILTER_GROUPS} + if (!site.propsEnabled) delete filterGroups.props + + return Object.keys(filterGroups).map((option) => filterDropdownOption(site, option)) } return ( diff --git a/lib/plausible/stats/query.ex b/lib/plausible/stats/query.ex index 6f46c59be4..6c52f79e9e 100644 --- a/lib/plausible/stats/query.ex +++ b/lib/plausible/stats/query.ex @@ -19,6 +19,7 @@ defmodule Plausible.Stats.Query do |> put_parsed_filters(params) |> put_imported_opts(site, params) |> put_sample_threshold(params) + |> maybe_drop_prop_filter(site) end defp query_by_period(site, %{"period" => "realtime"}) do @@ -228,6 +229,19 @@ defmodule Plausible.Stats.Query do |> Map.put(:include_imported, include_imported?(query, site, requested?)) end + defp maybe_drop_prop_filter(query, site) do + prop_filter? = Map.has_key?(query.filters, "props") + + props_available? = fn -> + site = Plausible.Repo.preload(site, :owner) + Plausible.Billing.Feature.Props.check_availability(site.owner) == :ok + end + + if prop_filter? && !props_available?.(), + do: %__MODULE__{query | filters: Map.drop(query.filters, ["props"])}, + else: query + end + @spec include_imported?(t(), Plausible.Site.t(), boolean()) :: boolean() def include_imported?(query, site, requested?) do cond do diff --git a/lib/plausible/stats/util.ex b/lib/plausible/stats/util.ex index c65adff709..7fe57132b4 100644 --- a/lib/plausible/stats/util.ex +++ b/lib/plausible/stats/util.ex @@ -29,8 +29,8 @@ defmodule Plausible.Stats.Util do {atom() | nil, [atom()]} @doc """ Returns the common currency for the goal filters in a query. If there are no - goal filters, or multiple currencies, `nil` is returned and revenue metrics - are dropped. + goal filters, multiple currencies or the site owner does not have access to + revenue goals, `nil` is returned and revenue metrics are dropped. Aggregating revenue data works only for same currency goals. If the query is filtered by goals with different currencies, for example, one USD and other @@ -44,7 +44,15 @@ defmodule Plausible.Stats.Util do _any -> [] end - if Enum.any?(metrics, &(&1 in @revenue_metrics)) && Enum.any?(goal_filters) do + requested_revenue_metrics? = Enum.any?(metrics, &(&1 in @revenue_metrics)) + filtering_by_goal? = Enum.any?(goal_filters) + + revenue_goals_available? = fn -> + site = Plausible.Repo.preload(site, :owner) + Plausible.Billing.Feature.RevenueGoals.check_availability(site.owner) == :ok + end + + if requested_revenue_metrics? && filtering_by_goal? && revenue_goals_available?.() do revenue_goals_currencies = Plausible.Repo.all( from rg in Ecto.assoc(site, :revenue_goals), diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 4568a096f2..c4a6824559 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -4,6 +4,7 @@ defmodule PlausibleWeb.Api.StatsController do use Plug.ErrorHandler alias Plausible.Stats alias Plausible.Stats.{Query, Filters, Comparisons} + alias PlausibleWeb.Api.Helpers, as: H require Logger @@ -513,9 +514,10 @@ defmodule PlausibleWeb.Api.StatsController do end def funnel(conn, %{"id" => funnel_id} = params) do - site = conn.assigns[:site] + site = Plausible.Repo.preload(conn.assigns.site, :owner) - with :ok <- validate_params(site, params), + with :ok <- Plausible.Billing.Feature.Funnels.check_availability(site.owner), + :ok <- validate_params(site, params), query <- Query.from(site, params) |> Filters.add_prefix(), :ok <- validate_funnel_query(query), {funnel_id, ""} <- Integer.parse(funnel_id), @@ -537,6 +539,12 @@ defmodule PlausibleWeb.Api.StatsController do |> json(%{error: "Funnel not found"}) |> halt() + {:error, :upgrade_required} -> + H.payment_required( + conn, + "#{Plausible.Billing.Feature.Funnels.display_name()} is part of the Plausible Business plan. To get access to this feature, please upgrade your account." + ) + _ -> bad_request(conn, "There was an error with your request") end @@ -1206,13 +1214,23 @@ defmodule PlausibleWeb.Api.StatsController do end def custom_prop_values(conn, params) do - site = conn.assigns[:site] - props = breakdown_custom_prop_values(site, params) - json(conn, props) + site = Plausible.Repo.preload(conn.assigns.site, :owner) + + case Plausible.Billing.Feature.Props.check_availability(site.owner) do + :ok -> + props = breakdown_custom_prop_values(site, params) + json(conn, props) + + {:error, :upgrade_required} -> + H.payment_required( + conn, + "#{Plausible.Billing.Feature.Props.display_name()} is part of the Plausible Business plan. To get access to this feature, please upgrade your account." + ) + end end def all_custom_prop_values(conn, params) do - site = conn.assigns[:site] + site = conn.assigns.site query = Query.from(site, params) |> Filters.add_prefix() prop_names = Plausible.Stats.CustomProps.fetch_prop_names(site, query) diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index 00818b6e77..126d1ca6de 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -49,6 +49,7 @@ defmodule PlausibleWeb.StatsController do plug(PlausibleWeb.AuthorizeSiteAccess when action in [:stats, :csv_export]) def stats(%{assigns: %{site: site}} = conn, _params) do + site = Plausible.Repo.preload(site, :owner) stats_start_date = Plausible.Sites.stats_start_date(site) can_see_stats? = not Sites.locked?(site) or conn.assigns[:current_user_role] == :super_admin demo = site.domain == PlausibleWeb.Endpoint.host() @@ -95,7 +96,7 @@ defmodule PlausibleWeb.StatsController do """ def csv_export(conn, params) do if is_nil(params["interval"]) or Plausible.Stats.Interval.valid?(params["interval"]) do - site = conn.assigns[:site] + site = Plausible.Repo.preload(conn.assigns.site, :owner) query = Query.from(site, params) |> Filters.add_prefix() metrics = @@ -144,10 +145,18 @@ defmodule PlausibleWeb.StatsController do 'operating_systems.csv' => fn -> Api.StatsController.operating_systems(conn, params) end, 'devices.csv' => fn -> Api.StatsController.screen_sizes(conn, params) end, 'conversions.csv' => fn -> Api.StatsController.conversions(conn, params) end, - 'referrers.csv' => fn -> Api.StatsController.referrers(conn, params) end, - 'custom_props.csv' => fn -> Api.StatsController.all_custom_prop_values(conn, params) end + 'referrers.csv' => fn -> Api.StatsController.referrers(conn, params) end } + csvs = + if Plausible.Billing.Feature.Props.enabled?(site) do + Map.put(csvs, 'custom_props.csv', fn -> + Api.StatsController.all_custom_prop_values(conn, params) + end) + else + csvs + end + csv_values = Map.values(csvs) |> Plausible.ClickhouseRepo.parallel_tasks() diff --git a/lib/plausible_web/templates/stats/stats.html.eex b/lib/plausible_web/templates/stats/stats.html.eex index f07d8e20bc..27ace8b91d 100644 --- a/lib/plausible_web/templates/stats/stats.html.eex +++ b/lib/plausible_web/templates/stats/stats.html.eex @@ -18,9 +18,9 @@ data-domain="<%= @site.domain %>" data-offset="<%= Plausible.Site.tz_offset(@site) %>" data-has-goals="<%= @has_goals %>" - data-conversions-enabled="<%= @site.conversions_enabled %>" - data-funnels-enabled="<%= @site.funnels_enabled %>" - data-props-enabled="<%= @site.props_enabled %>" + data-conversions-enabled="<%= Plausible.Billing.Feature.Goals.enabled?(@site) %>" + data-funnels-enabled="<%= Plausible.Billing.Feature.Funnels.enabled?(@site) %>" + data-props-enabled="<%= Plausible.Billing.Feature.Props.enabled?(@site) %>" data-funnels="<%= Jason.encode!(@funnels) %>" data-has-props="<%= @has_props %>" data-logged-in="<%= !!@conn.assigns[:current_user] %>" diff --git a/test/plausible/debug_replay_info_test.exs b/test/plausible/debug_replay_info_test.exs index fb424af511..e178aebaaf 100644 --- a/test/plausible/debug_replay_info_test.exs +++ b/test/plausible/debug_replay_info_test.exs @@ -12,7 +12,7 @@ defmodule Plausible.DebugReplayInfoTest do end test "adds replayable sentry context" do - site = build(:site) + site = insert(:site) query = Plausible.Stats.Query.from(site, %{"period" => "day"}) {:ok, {^site, ^query}} = SampleModule.task(site, query, self()) diff --git a/test/plausible/stats/comparisons_test.exs b/test/plausible/stats/comparisons_test.exs index c0a8e8d0e4..d0875bf2db 100644 --- a/test/plausible/stats/comparisons_test.exs +++ b/test/plausible/stats/comparisons_test.exs @@ -5,7 +5,7 @@ defmodule Plausible.Stats.ComparisonsTest do describe "with period set to this month" do test "shifts back this month period when mode is previous_period" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "month", "date" => "2023-03-02"}) now = ~N[2023-03-02 14:00:00] @@ -16,7 +16,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "shifts back this month period when it's the first day of the month and mode is previous_period" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "month", "date" => "2023-03-01"}) now = ~N[2023-03-01 14:00:00] @@ -27,7 +27,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "matches the day of the week when nearest day is original query start date and mode is previous_period" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "month", "date" => "2023-03-02"}) now = ~N[2023-03-02 14:00:00] @@ -41,7 +41,7 @@ defmodule Plausible.Stats.ComparisonsTest do describe "with period set to previous month" do test "shifts back using the same number of days when mode is previous_period" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"}) now = ~N[2023-03-01 14:00:00] @@ -52,7 +52,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "shifts back the full month when mode is year_over_year" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"}) now = ~N[2023-03-01 14:00:00] @@ -63,7 +63,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "shifts back whole month plus one day when mode is year_over_year and a leap year" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "month", "date" => "2020-02-01"}) now = ~N[2023-03-01 14:00:00] @@ -74,7 +74,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "matches the day of the week when mode is previous_period keeping the same day" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"}) now = ~N[2023-03-01 14:00:00] @@ -86,7 +86,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "matches the day of the week when mode is previous_period" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "month", "date" => "2023-01-01"}) now = ~N[2023-03-01 14:00:00] @@ -100,7 +100,7 @@ defmodule Plausible.Stats.ComparisonsTest do describe "with period set to year to date" do test "shifts back by the same number of days when mode is previous_period" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"}) now = ~N[2023-03-01 14:00:00] @@ -111,7 +111,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "shifts back by the same number of days when mode is year_over_year" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"}) now = ~N[2023-03-01 14:00:00] @@ -122,7 +122,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "matches the day of the week when mode is year_over_year" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"}) now = ~N[2023-03-01 14:00:00] @@ -136,7 +136,7 @@ defmodule Plausible.Stats.ComparisonsTest do describe "with period set to previous year" do test "shifts back a whole year when mode is year_over_year" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"}) {:ok, comparison} = Comparisons.compare(site, query, "year_over_year") @@ -146,7 +146,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "shifts back a whole year when mode is previous_period" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"}) {:ok, comparison} = Comparisons.compare(site, query, "previous_period") @@ -158,7 +158,7 @@ defmodule Plausible.Stats.ComparisonsTest do describe "with period set to custom" do test "shifts back by the same number of days when mode is previous_period" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"}) {:ok, comparison} = Comparisons.compare(site, query, "previous_period") @@ -168,7 +168,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "shifts back to last year when mode is year_over_year" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"}) {:ok, comparison} = Comparisons.compare(site, query, "year_over_year") @@ -180,7 +180,7 @@ defmodule Plausible.Stats.ComparisonsTest do describe "with mode set to custom" do test "sets first and last dates" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"}) {:ok, comparison} = @@ -191,7 +191,7 @@ defmodule Plausible.Stats.ComparisonsTest do end test "validates from and to dates" do - site = build(:site) + site = insert(:site) query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"}) assert {:error, :invalid_dates} == diff --git a/test/plausible/stats/query_test.exs b/test/plausible/stats/query_test.exs index ca4e841c18..1974de61bc 100644 --- a/test/plausible/stats/query_test.exs +++ b/test/plausible/stats/query_test.exs @@ -1,48 +1,56 @@ defmodule Plausible.Stats.QueryTest do - use ExUnit.Case, async: true + use Plausible.DataCase, async: true alias Plausible.Stats.Query + @v4_growth_plan_id "change-me-749342" + @v4_business_plan_id "change-me-b749342" - @site_inserted_at ~D[2020-01-01] - @site %Plausible.Site{ - timezone: "UTC", - inserted_at: @site_inserted_at, - stats_start_date: @site_inserted_at - } + setup do + user = insert(:user) - test "parses day format" do - q = Query.from(@site, %{"period" => "day", "date" => "2019-01-01"}) + site = + insert(:site, + members: [user], + inserted_at: ~N[2020-01-01T00:00:00], + stats_start_date: ~D[2020-01-01] + ) + + {:ok, site: site, user: user} + end + + test "parses day format", %{site: site} do + q = Query.from(site, %{"period" => "day", "date" => "2019-01-01"}) assert q.date_range.first == ~D[2019-01-01] assert q.date_range.last == ~D[2019-01-01] assert q.interval == "hour" end - test "day format defaults to today" do - q = Query.from(@site, %{"period" => "day"}) + test "day format defaults to today", %{site: site} do + q = Query.from(site, %{"period" => "day"}) assert q.date_range.first == Timex.today() assert q.date_range.last == Timex.today() assert q.interval == "hour" end - test "parses realtime format" do - q = Query.from(@site, %{"period" => "realtime"}) + test "parses realtime format", %{site: site} do + q = Query.from(site, %{"period" => "realtime"}) assert q.date_range.first == Timex.today() assert q.date_range.last == Timex.today() assert q.period == "realtime" end - test "parses month format" do - q = Query.from(@site, %{"period" => "month", "date" => "2019-01-01"}) + test "parses month format", %{site: site} do + q = Query.from(site, %{"period" => "month", "date" => "2019-01-01"}) assert q.date_range.first == ~D[2019-01-01] assert q.date_range.last == ~D[2019-01-31] assert q.interval == "date" end - test "parses 6 month format" do - q = Query.from(@site, %{"period" => "6mo"}) + test "parses 6 month format", %{site: site} do + q = Query.from(site, %{"period" => "6mo"}) assert q.date_range.first == Timex.shift(Timex.today(), months: -5) |> Timex.beginning_of_month() @@ -51,8 +59,8 @@ defmodule Plausible.Stats.QueryTest do assert q.interval == "month" end - test "parses 12 month format" do - q = Query.from(@site, %{"period" => "12mo"}) + test "parses 12 month format", %{site: site} do + q = Query.from(site, %{"period" => "12mo"}) assert q.date_range.first == Timex.shift(Timex.today(), months: -11) |> Timex.beginning_of_month() @@ -61,37 +69,37 @@ defmodule Plausible.Stats.QueryTest do assert q.interval == "month" end - test "parses year to date format" do - q = Query.from(@site, %{"period" => "year"}) + test "parses year to date format", %{site: site} do + q = Query.from(site, %{"period" => "year"}) assert q.date_range.first == - Timex.now(@site.timezone) |> Timex.to_date() |> Timex.beginning_of_year() + Timex.now(site.timezone) |> Timex.to_date() |> Timex.beginning_of_year() assert q.date_range.last == - Timex.now(@site.timezone) |> Timex.to_date() |> Timex.end_of_year() + Timex.now(site.timezone) |> Timex.to_date() |> Timex.end_of_year() assert q.interval == "month" end - test "parses all time" do - q = Query.from(@site, %{"period" => "all"}) + test "parses all time", %{site: site} do + q = Query.from(site, %{"period" => "all"}) - assert q.date_range.first == @site_inserted_at + assert q.date_range.first == NaiveDateTime.to_date(site.inserted_at) assert q.date_range.last == Timex.today() assert q.period == "all" assert q.interval == "month" end - test "parses all time in correct timezone" do - site = Map.put(@site, :timezone, "America/Cancun") + test "parses all time in correct timezone", %{site: site} do + site = Map.put(site, :timezone, "America/Cancun") q = Query.from(site, %{"period" => "all"}) assert q.date_range.first == ~D[2019-12-31] assert q.date_range.last == Timex.today("America/Cancun") end - test "all time shows today if site has no start date" do - site = Map.put(@site, :stats_start_date, nil) + test "all time shows today if site has no start date", %{site: site} do + site = Map.put(site, :stats_start_date, nil) q = Query.from(site, %{"period" => "all"}) assert q.date_range.first == Timex.today() @@ -100,8 +108,8 @@ defmodule Plausible.Stats.QueryTest do assert q.interval == "hour" end - test "all time shows hourly if site is completely new" do - site = Map.put(@site, :stats_start_date, Timex.now()) + test "all time shows hourly if site is completely new", %{site: site} do + site = Map.put(site, :stats_start_date, Timex.now()) q = Query.from(site, %{"period" => "all"}) assert q.date_range.first == Timex.today() @@ -110,8 +118,8 @@ defmodule Plausible.Stats.QueryTest do assert q.interval == "hour" end - test "all time shows daily if site is more than a day old" do - site = Map.put(@site, :stats_start_date, Timex.now() |> Timex.shift(days: -1)) + test "all time shows daily if site is more than a day old", %{site: site} do + site = Map.put(site, :stats_start_date, Timex.now() |> Timex.shift(days: -1)) q = Query.from(site, %{"period" => "all"}) assert q.date_range.first == Timex.today() |> Timex.shift(days: -1) @@ -120,8 +128,8 @@ defmodule Plausible.Stats.QueryTest do assert q.interval == "date" end - test "all time shows monthly if site is more than a month old" do - site = Map.put(@site, :stats_start_date, Timex.now() |> Timex.shift(months: -1)) + test "all time shows monthly if site is more than a month old", %{site: site} do + site = Map.put(site, :stats_start_date, Timex.now() |> Timex.shift(months: -1)) q = Query.from(site, %{"period" => "all"}) assert q.date_range.first == Timex.today() |> Timex.shift(months: -1) @@ -130,8 +138,8 @@ defmodule Plausible.Stats.QueryTest do assert q.interval == "month" end - test "all time uses passed interval different from the default interval" do - site = Map.put(@site, :stats_start_date, Timex.now() |> Timex.shift(months: -1)) + test "all time uses passed interval different from the default interval", %{site: site} do + site = Map.put(site, :stats_start_date, Timex.now() |> Timex.shift(months: -1)) q = Query.from(site, %{"period" => "all", "interval" => "week"}) assert q.date_range.first == Timex.today() |> Timex.shift(months: -1) @@ -140,41 +148,57 @@ defmodule Plausible.Stats.QueryTest do assert q.interval == "week" end - test "defaults to 30 days format" do - assert Query.from(@site, %{}) == Query.from(@site, %{"period" => "30d"}) + test "defaults to 30 days format", %{site: site} do + assert Query.from(site, %{}) == Query.from(site, %{"period" => "30d"}) end - test "parses custom format" do - q = Query.from(@site, %{"period" => "custom", "from" => "2019-01-01", "to" => "2019-01-15"}) + test "parses custom format", %{site: site} do + q = Query.from(site, %{"period" => "custom", "from" => "2019-01-01", "to" => "2019-01-15"}) assert q.date_range.first == ~D[2019-01-01] assert q.date_range.last == ~D[2019-01-15] assert q.interval == "date" end - test "adds sample_threshold :infinite to query struct" do - q = Query.from(@site, %{"period" => "30d", "sample_threshold" => "infinite"}) + test "adds sample_threshold :infinite to query struct", %{site: site} do + q = Query.from(site, %{"period" => "30d", "sample_threshold" => "infinite"}) assert q.sample_threshold == :infinite end - test "casts sample_threshold to integer in query struct" do - q = Query.from(@site, %{"period" => "30d", "sample_threshold" => "30000000"}) + test "casts sample_threshold to integer in query struct", %{site: site} do + q = Query.from(site, %{"period" => "30d", "sample_threshold" => "30000000"}) assert q.sample_threshold == 30_000_000 end describe "filters" do - test "parses goal filter" do + test "parses goal filter", %{site: site} do filters = Jason.encode!(%{"goal" => "Signup"}) - q = Query.from(@site, %{"period" => "6mo", "filters" => filters}) + q = Query.from(site, %{"period" => "6mo", "filters" => filters}) assert q.filters["goal"] == "Signup" end - test "parses source filter" do + test "parses source filter", %{site: site} do filters = Jason.encode!(%{"source" => "Twitter"}) - q = Query.from(@site, %{"period" => "6mo", "filters" => filters}) + q = Query.from(site, %{"period" => "6mo", "filters" => filters}) assert q.filters["source"] == "Twitter" end + + test "allows prop filters when site owner is on a business plan", %{site: site, user: user} do + insert(:subscription, user: user, paddle_plan_id: @v4_business_plan_id) + filters = Jason.encode!(%{"props" => %{"author" => "!John Doe"}}) + query = Query.from(site, %{"period" => "6mo", "filters" => filters}) + + assert Map.has_key?(query.filters, "props") + end + + test "drops prop filter when site owner is on a growth plan", %{site: site, user: user} do + insert(:subscription, user: user, paddle_plan_id: @v4_growth_plan_id) + filters = Jason.encode!(%{"props" => %{"author" => "!John Doe"}}) + query = Query.from(site, %{"period" => "6mo", "filters" => filters}) + + refute Map.has_key?(query.filters, "props") + end end end diff --git a/test/plausible_web/controllers/api/stats_controller/custom_prop_breakdown_test.exs b/test/plausible_web/controllers/api/stats_controller/custom_prop_breakdown_test.exs index c7cd3c5590..d30c42aea6 100644 --- a/test/plausible_web/controllers/api/stats_controller/custom_prop_breakdown_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/custom_prop_breakdown_test.exs @@ -1,5 +1,6 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do use PlausibleWeb.ConnCase + @v4_growth_plan_id "change-me-749342" describe "GET /api/stats/:domain/custom-prop-values/:prop_key" do setup [:create_user, :log_in, :create_new_site, :add_imported_data] @@ -177,6 +178,17 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do } ] end + + test "errors when site owner is on a growth plan", %{conn: conn, site: site, user: user} do + insert(:subscription, user: user, paddle_plan_id: @v4_growth_plan_id) + + conn = get(conn, "/api/stats/#{site.domain}/custom-prop-values/prop?period=day") + + assert json_response(conn, 402) == %{ + "error" => + "Custom Properties is part of the Plausible Business plan. To get access to this feature, please upgrade your account." + } + end end describe "GET /api/stats/:domain/custom-prop-values/:prop_key - with goal filter" do 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 1a536bcb93..6c62012eea 100644 --- a/test/plausible_web/controllers/api/stats_controller/funnels_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/funnels_test.exs @@ -3,6 +3,7 @@ defmodule PlausibleWeb.Api.StatsController.FunnelsTest do @user_id 123 @other_user_id 456 + @v4_growth_plan_id "change-me-749342" @build_funnel_with [ {"page_path", "/blog/announcement"}, @@ -219,6 +220,25 @@ defmodule PlausibleWeb.Api.StatsController.FunnelsTest do ] } = resp end + + test "returns HTTP 402 when site owner is on a growth plan", %{ + conn: conn, + user: user, + site: site + } do + insert(:subscription, user: user, paddle_plan_id: @v4_growth_plan_id) + {:ok, funnel} = setup_funnel(site, @build_funnel_with) + + resp = + conn + |> get("/api/stats/#{site.domain}/funnels/#{funnel.id}/?period=day") + |> json_response(402) + + assert %{ + "error" => + "Funnels 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 diff --git a/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs b/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs index 0ccd276bfc..cfa27f09ae 100644 --- a/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs @@ -1,6 +1,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do use PlausibleWeb.ConnCase + @v4_growth_plan_id "change-me-749342" @user_id 123 describe "GET /api/stats/top-stats - default" do @@ -825,6 +826,28 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do refute "Average revenue" in metrics refute "Total revenue" in metrics end + + test "does not return average and total when site owner is on a growth plan", + %{conn: conn, site: site, user: user} do + insert(:subscription, user: user, paddle_plan_id: @v4_growth_plan_id) + insert(:goal, site: site, event_name: "Payment", currency: "USD") + + populate_stats(site, [ + build(:event, + name: "Payment", + revenue_reporting_amount: Decimal.new(13_29), + revenue_reporting_currency: "USD" + ) + ]) + + filters = Jason.encode!(%{goal: "Payment"}) + conn = get(conn, "/api/stats/#{site.domain}/top-stats?period=all&filters=#{filters}") + assert %{"top_stats" => top_stats} = json_response(conn, 200) + + metrics = Enum.map(top_stats, & &1["name"]) + refute "Average revenue" in metrics + refute "Total revenue" in metrics + end end describe "GET /api/stats/top-stats - with comparisons" do diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs index 41e69e0116..c2b8724a0e 100644 --- a/test/plausible_web/controllers/stats_controller_test.exs +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -2,6 +2,7 @@ defmodule PlausibleWeb.StatsControllerTest do use PlausibleWeb.ConnCase, async: false use Plausible.Repo import Plausible.Test.Support.HTML + @v4_growth_plan_id "change-me-749342" describe "GET /:website - anonymous user" do test "public site - shows site stats", %{conn: conn} do @@ -154,6 +155,20 @@ defmodule PlausibleWeb.StatsControllerTest do assert 'utm_terms.csv' in zip end + test "does not export custom properties when site owner is on a growth plan", %{ + conn: conn, + site: site, + user: user + } do + insert(:subscription, user: user, paddle_plan_id: @v4_growth_plan_id) + response = conn |> get("/" <> site.domain <> "/export") |> response(200) + + {:ok, zip} = :zip.unzip(response, [:memory]) + files = Map.new(zip) + + refute Map.has_key?(files, 'custom_props.csv') + end + test "exports data in zipped csvs", %{conn: conn, site: site} do populate_exported_stats(site) conn = get(conn, "/" <> site.domain <> "/export?date=2021-10-20")