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
This commit is contained in:
Vini Brasil 2023-10-17 10:00:00 -03:00 committed by GitHub
parent 9b912f3d89
commit 896d78d8fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 230 additions and 85 deletions

View File

@ -94,8 +94,10 @@ function DropdownContent({ history, site, query, wrapped }) {
const [addingFilter, setAddingFilter] = useState(false); const [addingFilter, setAddingFilter] = useState(false);
if (wrapped === 0 || addingFilter) { if (wrapped === 0 || addingFilter) {
return Object.keys(FILTER_GROUPS) let filterGroups = {...FILTER_GROUPS}
.map((option) => filterDropdownOption(site, option)) if (!site.propsEnabled) delete filterGroups.props
return Object.keys(filterGroups).map((option) => filterDropdownOption(site, option))
} }
return ( return (

View File

@ -19,6 +19,7 @@ defmodule Plausible.Stats.Query do
|> put_parsed_filters(params) |> put_parsed_filters(params)
|> put_imported_opts(site, params) |> put_imported_opts(site, params)
|> put_sample_threshold(params) |> put_sample_threshold(params)
|> maybe_drop_prop_filter(site)
end end
defp query_by_period(site, %{"period" => "realtime"}) do 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?)) |> Map.put(:include_imported, include_imported?(query, site, requested?))
end 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() @spec include_imported?(t(), Plausible.Site.t(), boolean()) :: boolean()
def include_imported?(query, site, requested?) do def include_imported?(query, site, requested?) do
cond do cond do

View File

@ -29,8 +29,8 @@ defmodule Plausible.Stats.Util do
{atom() | nil, [atom()]} {atom() | nil, [atom()]}
@doc """ @doc """
Returns the common currency for the goal filters in a query. If there are no 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 goal filters, multiple currencies or the site owner does not have access to
are dropped. revenue goals, `nil` is returned and revenue metrics are dropped.
Aggregating revenue data works only for same currency goals. If the query is 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 filtered by goals with different currencies, for example, one USD and other
@ -44,7 +44,15 @@ defmodule Plausible.Stats.Util do
_any -> [] _any -> []
end 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 = revenue_goals_currencies =
Plausible.Repo.all( Plausible.Repo.all(
from rg in Ecto.assoc(site, :revenue_goals), from rg in Ecto.assoc(site, :revenue_goals),

View File

@ -4,6 +4,7 @@ defmodule PlausibleWeb.Api.StatsController do
use Plug.ErrorHandler use Plug.ErrorHandler
alias Plausible.Stats alias Plausible.Stats
alias Plausible.Stats.{Query, Filters, Comparisons} alias Plausible.Stats.{Query, Filters, Comparisons}
alias PlausibleWeb.Api.Helpers, as: H
require Logger require Logger
@ -513,9 +514,10 @@ defmodule PlausibleWeb.Api.StatsController do
end end
def funnel(conn, %{"id" => funnel_id} = params) do 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(), query <- Query.from(site, params) |> Filters.add_prefix(),
:ok <- validate_funnel_query(query), :ok <- validate_funnel_query(query),
{funnel_id, ""} <- Integer.parse(funnel_id), {funnel_id, ""} <- Integer.parse(funnel_id),
@ -537,6 +539,12 @@ defmodule PlausibleWeb.Api.StatsController do
|> json(%{error: "Funnel not found"}) |> json(%{error: "Funnel not found"})
|> halt() |> 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") bad_request(conn, "There was an error with your request")
end end
@ -1206,13 +1214,23 @@ defmodule PlausibleWeb.Api.StatsController do
end end
def custom_prop_values(conn, params) do def custom_prop_values(conn, params) do
site = conn.assigns[:site] site = Plausible.Repo.preload(conn.assigns.site, :owner)
props = breakdown_custom_prop_values(site, params)
json(conn, props) 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 end
def all_custom_prop_values(conn, params) do def all_custom_prop_values(conn, params) do
site = conn.assigns[:site] site = conn.assigns.site
query = Query.from(site, params) |> Filters.add_prefix() query = Query.from(site, params) |> Filters.add_prefix()
prop_names = Plausible.Stats.CustomProps.fetch_prop_names(site, query) prop_names = Plausible.Stats.CustomProps.fetch_prop_names(site, query)

View File

@ -49,6 +49,7 @@ defmodule PlausibleWeb.StatsController do
plug(PlausibleWeb.AuthorizeSiteAccess when action in [:stats, :csv_export]) plug(PlausibleWeb.AuthorizeSiteAccess when action in [:stats, :csv_export])
def stats(%{assigns: %{site: site}} = conn, _params) do def stats(%{assigns: %{site: site}} = conn, _params) do
site = Plausible.Repo.preload(site, :owner)
stats_start_date = Plausible.Sites.stats_start_date(site) stats_start_date = Plausible.Sites.stats_start_date(site)
can_see_stats? = not Sites.locked?(site) or conn.assigns[:current_user_role] == :super_admin can_see_stats? = not Sites.locked?(site) or conn.assigns[:current_user_role] == :super_admin
demo = site.domain == PlausibleWeb.Endpoint.host() demo = site.domain == PlausibleWeb.Endpoint.host()
@ -95,7 +96,7 @@ defmodule PlausibleWeb.StatsController do
""" """
def csv_export(conn, params) do def csv_export(conn, params) do
if is_nil(params["interval"]) or Plausible.Stats.Interval.valid?(params["interval"]) 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() query = Query.from(site, params) |> Filters.add_prefix()
metrics = metrics =
@ -144,10 +145,18 @@ defmodule PlausibleWeb.StatsController do
'operating_systems.csv' => fn -> Api.StatsController.operating_systems(conn, params) end, 'operating_systems.csv' => fn -> Api.StatsController.operating_systems(conn, params) end,
'devices.csv' => fn -> Api.StatsController.screen_sizes(conn, params) end, 'devices.csv' => fn -> Api.StatsController.screen_sizes(conn, params) end,
'conversions.csv' => fn -> Api.StatsController.conversions(conn, params) end, 'conversions.csv' => fn -> Api.StatsController.conversions(conn, params) end,
'referrers.csv' => fn -> Api.StatsController.referrers(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
} }
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 = csv_values =
Map.values(csvs) Map.values(csvs)
|> Plausible.ClickhouseRepo.parallel_tasks() |> Plausible.ClickhouseRepo.parallel_tasks()

View File

@ -18,9 +18,9 @@
data-domain="<%= @site.domain %>" data-domain="<%= @site.domain %>"
data-offset="<%= Plausible.Site.tz_offset(@site) %>" data-offset="<%= Plausible.Site.tz_offset(@site) %>"
data-has-goals="<%= @has_goals %>" data-has-goals="<%= @has_goals %>"
data-conversions-enabled="<%= @site.conversions_enabled %>" data-conversions-enabled="<%= Plausible.Billing.Feature.Goals.enabled?(@site) %>"
data-funnels-enabled="<%= @site.funnels_enabled %>" data-funnels-enabled="<%= Plausible.Billing.Feature.Funnels.enabled?(@site) %>"
data-props-enabled="<%= @site.props_enabled %>" data-props-enabled="<%= Plausible.Billing.Feature.Props.enabled?(@site) %>"
data-funnels="<%= Jason.encode!(@funnels) %>" data-funnels="<%= Jason.encode!(@funnels) %>"
data-has-props="<%= @has_props %>" data-has-props="<%= @has_props %>"
data-logged-in="<%= !!@conn.assigns[:current_user] %>" data-logged-in="<%= !!@conn.assigns[:current_user] %>"

View File

@ -12,7 +12,7 @@ defmodule Plausible.DebugReplayInfoTest do
end end
test "adds replayable sentry context" do test "adds replayable sentry context" do
site = build(:site) site = insert(:site)
query = Plausible.Stats.Query.from(site, %{"period" => "day"}) query = Plausible.Stats.Query.from(site, %{"period" => "day"})
{:ok, {^site, ^query}} = SampleModule.task(site, query, self()) {:ok, {^site, ^query}} = SampleModule.task(site, query, self())

View File

@ -5,7 +5,7 @@ defmodule Plausible.Stats.ComparisonsTest do
describe "with period set to this month" do describe "with period set to this month" do
test "shifts back this month period when mode is previous_period" 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"}) query = Query.from(site, %{"period" => "month", "date" => "2023-03-02"})
now = ~N[2023-03-02 14:00:00] now = ~N[2023-03-02 14:00:00]
@ -16,7 +16,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "shifts back this month period when it's the first day of the month and mode is previous_period" do 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"}) query = Query.from(site, %{"period" => "month", "date" => "2023-03-01"})
now = ~N[2023-03-01 14:00:00] now = ~N[2023-03-01 14:00:00]
@ -27,7 +27,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "matches the day of the week when nearest day is original query start date and mode is previous_period" do 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"}) query = Query.from(site, %{"period" => "month", "date" => "2023-03-02"})
now = ~N[2023-03-02 14:00:00] 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 describe "with period set to previous month" do
test "shifts back using the same number of days when mode is previous_period" 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"}) query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
now = ~N[2023-03-01 14:00:00] now = ~N[2023-03-01 14:00:00]
@ -52,7 +52,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "shifts back the full month when mode is year_over_year" do 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"}) query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
now = ~N[2023-03-01 14:00:00] now = ~N[2023-03-01 14:00:00]
@ -63,7 +63,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "shifts back whole month plus one day when mode is year_over_year and a leap year" do 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"}) query = Query.from(site, %{"period" => "month", "date" => "2020-02-01"})
now = ~N[2023-03-01 14:00:00] now = ~N[2023-03-01 14:00:00]
@ -74,7 +74,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "matches the day of the week when mode is previous_period keeping the same day" do 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"}) query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
now = ~N[2023-03-01 14:00:00] now = ~N[2023-03-01 14:00:00]
@ -86,7 +86,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "matches the day of the week when mode is previous_period" do 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"}) query = Query.from(site, %{"period" => "month", "date" => "2023-01-01"})
now = ~N[2023-03-01 14:00:00] 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 describe "with period set to year to date" do
test "shifts back by the same number of days when mode is previous_period" 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"}) query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
now = ~N[2023-03-01 14:00:00] now = ~N[2023-03-01 14:00:00]
@ -111,7 +111,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "shifts back by the same number of days when mode is year_over_year" do 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"}) query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
now = ~N[2023-03-01 14:00:00] now = ~N[2023-03-01 14:00:00]
@ -122,7 +122,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "matches the day of the week when mode is year_over_year" do 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"}) query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
now = ~N[2023-03-01 14:00:00] 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 describe "with period set to previous year" do
test "shifts back a whole year when mode is year_over_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"}) query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year") {:ok, comparison} = Comparisons.compare(site, query, "year_over_year")
@ -146,7 +146,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "shifts back a whole year when mode is previous_period" do 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"}) query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
{:ok, comparison} = Comparisons.compare(site, query, "previous_period") {:ok, comparison} = Comparisons.compare(site, query, "previous_period")
@ -158,7 +158,7 @@ defmodule Plausible.Stats.ComparisonsTest do
describe "with period set to custom" do describe "with period set to custom" do
test "shifts back by the same number of days when mode is previous_period" 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"}) query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
{:ok, comparison} = Comparisons.compare(site, query, "previous_period") {:ok, comparison} = Comparisons.compare(site, query, "previous_period")
@ -168,7 +168,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "shifts back to last year when mode is year_over_year" do 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"}) query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year") {: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 describe "with mode set to custom" do
test "sets first and last dates" 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"}) query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
{:ok, comparison} = {:ok, comparison} =
@ -191,7 +191,7 @@ defmodule Plausible.Stats.ComparisonsTest do
end end
test "validates from and to dates" do 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"}) query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
assert {:error, :invalid_dates} == assert {:error, :invalid_dates} ==

View File

@ -1,48 +1,56 @@
defmodule Plausible.Stats.QueryTest do defmodule Plausible.Stats.QueryTest do
use ExUnit.Case, async: true use Plausible.DataCase, async: true
alias Plausible.Stats.Query 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] setup do
@site %Plausible.Site{ user = insert(:user)
timezone: "UTC",
inserted_at: @site_inserted_at,
stats_start_date: @site_inserted_at
}
test "parses day format" do site =
q = Query.from(@site, %{"period" => "day", "date" => "2019-01-01"}) 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.first == ~D[2019-01-01]
assert q.date_range.last == ~D[2019-01-01] assert q.date_range.last == ~D[2019-01-01]
assert q.interval == "hour" assert q.interval == "hour"
end end
test "day format defaults to today" do test "day format defaults to today", %{site: site} do
q = Query.from(@site, %{"period" => "day"}) q = Query.from(site, %{"period" => "day"})
assert q.date_range.first == Timex.today() assert q.date_range.first == Timex.today()
assert q.date_range.last == Timex.today() assert q.date_range.last == Timex.today()
assert q.interval == "hour" assert q.interval == "hour"
end end
test "parses realtime format" do test "parses realtime format", %{site: site} do
q = Query.from(@site, %{"period" => "realtime"}) q = Query.from(site, %{"period" => "realtime"})
assert q.date_range.first == Timex.today() assert q.date_range.first == Timex.today()
assert q.date_range.last == Timex.today() assert q.date_range.last == Timex.today()
assert q.period == "realtime" assert q.period == "realtime"
end end
test "parses month format" do test "parses month format", %{site: site} do
q = Query.from(@site, %{"period" => "month", "date" => "2019-01-01"}) q = Query.from(site, %{"period" => "month", "date" => "2019-01-01"})
assert q.date_range.first == ~D[2019-01-01] assert q.date_range.first == ~D[2019-01-01]
assert q.date_range.last == ~D[2019-01-31] assert q.date_range.last == ~D[2019-01-31]
assert q.interval == "date" assert q.interval == "date"
end end
test "parses 6 month format" do test "parses 6 month format", %{site: site} do
q = Query.from(@site, %{"period" => "6mo"}) q = Query.from(site, %{"period" => "6mo"})
assert q.date_range.first == assert q.date_range.first ==
Timex.shift(Timex.today(), months: -5) |> Timex.beginning_of_month() Timex.shift(Timex.today(), months: -5) |> Timex.beginning_of_month()
@ -51,8 +59,8 @@ defmodule Plausible.Stats.QueryTest do
assert q.interval == "month" assert q.interval == "month"
end end
test "parses 12 month format" do test "parses 12 month format", %{site: site} do
q = Query.from(@site, %{"period" => "12mo"}) q = Query.from(site, %{"period" => "12mo"})
assert q.date_range.first == assert q.date_range.first ==
Timex.shift(Timex.today(), months: -11) |> Timex.beginning_of_month() Timex.shift(Timex.today(), months: -11) |> Timex.beginning_of_month()
@ -61,37 +69,37 @@ defmodule Plausible.Stats.QueryTest do
assert q.interval == "month" assert q.interval == "month"
end end
test "parses year to date format" do test "parses year to date format", %{site: site} do
q = Query.from(@site, %{"period" => "year"}) q = Query.from(site, %{"period" => "year"})
assert q.date_range.first == 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 == 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" assert q.interval == "month"
end end
test "parses all time" do test "parses all time", %{site: site} do
q = Query.from(@site, %{"period" => "all"}) 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.date_range.last == Timex.today()
assert q.period == "all" assert q.period == "all"
assert q.interval == "month" assert q.interval == "month"
end end
test "parses all time in correct timezone" do test "parses all time in correct timezone", %{site: site} do
site = Map.put(@site, :timezone, "America/Cancun") site = Map.put(site, :timezone, "America/Cancun")
q = Query.from(site, %{"period" => "all"}) q = Query.from(site, %{"period" => "all"})
assert q.date_range.first == ~D[2019-12-31] assert q.date_range.first == ~D[2019-12-31]
assert q.date_range.last == Timex.today("America/Cancun") assert q.date_range.last == Timex.today("America/Cancun")
end end
test "all time shows today if site has no start date" do test "all time shows today if site has no start date", %{site: site} do
site = Map.put(@site, :stats_start_date, nil) site = Map.put(site, :stats_start_date, nil)
q = Query.from(site, %{"period" => "all"}) q = Query.from(site, %{"period" => "all"})
assert q.date_range.first == Timex.today() assert q.date_range.first == Timex.today()
@ -100,8 +108,8 @@ defmodule Plausible.Stats.QueryTest do
assert q.interval == "hour" assert q.interval == "hour"
end end
test "all time shows hourly if site is completely new" do test "all time shows hourly if site is completely new", %{site: site} do
site = Map.put(@site, :stats_start_date, Timex.now()) site = Map.put(site, :stats_start_date, Timex.now())
q = Query.from(site, %{"period" => "all"}) q = Query.from(site, %{"period" => "all"})
assert q.date_range.first == Timex.today() assert q.date_range.first == Timex.today()
@ -110,8 +118,8 @@ defmodule Plausible.Stats.QueryTest do
assert q.interval == "hour" assert q.interval == "hour"
end end
test "all time shows daily if site is more than a day old" do 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)) site = Map.put(site, :stats_start_date, Timex.now() |> Timex.shift(days: -1))
q = Query.from(site, %{"period" => "all"}) q = Query.from(site, %{"period" => "all"})
assert q.date_range.first == Timex.today() |> Timex.shift(days: -1) assert q.date_range.first == Timex.today() |> Timex.shift(days: -1)
@ -120,8 +128,8 @@ defmodule Plausible.Stats.QueryTest do
assert q.interval == "date" assert q.interval == "date"
end end
test "all time shows monthly if site is more than a month old" do 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)) site = Map.put(site, :stats_start_date, Timex.now() |> Timex.shift(months: -1))
q = Query.from(site, %{"period" => "all"}) q = Query.from(site, %{"period" => "all"})
assert q.date_range.first == Timex.today() |> Timex.shift(months: -1) assert q.date_range.first == Timex.today() |> Timex.shift(months: -1)
@ -130,8 +138,8 @@ defmodule Plausible.Stats.QueryTest do
assert q.interval == "month" assert q.interval == "month"
end end
test "all time uses passed interval different from the default interval" do 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)) site = Map.put(site, :stats_start_date, Timex.now() |> Timex.shift(months: -1))
q = Query.from(site, %{"period" => "all", "interval" => "week"}) q = Query.from(site, %{"period" => "all", "interval" => "week"})
assert q.date_range.first == Timex.today() |> Timex.shift(months: -1) assert q.date_range.first == Timex.today() |> Timex.shift(months: -1)
@ -140,41 +148,57 @@ defmodule Plausible.Stats.QueryTest do
assert q.interval == "week" assert q.interval == "week"
end end
test "defaults to 30 days format" do test "defaults to 30 days format", %{site: site} do
assert Query.from(@site, %{}) == Query.from(@site, %{"period" => "30d"}) assert Query.from(site, %{}) == Query.from(site, %{"period" => "30d"})
end end
test "parses custom format" do test "parses custom format", %{site: site} do
q = Query.from(@site, %{"period" => "custom", "from" => "2019-01-01", "to" => "2019-01-15"}) 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.first == ~D[2019-01-01]
assert q.date_range.last == ~D[2019-01-15] assert q.date_range.last == ~D[2019-01-15]
assert q.interval == "date" assert q.interval == "date"
end end
test "adds sample_threshold :infinite to query struct" do test "adds sample_threshold :infinite to query struct", %{site: site} do
q = Query.from(@site, %{"period" => "30d", "sample_threshold" => "infinite"}) q = Query.from(site, %{"period" => "30d", "sample_threshold" => "infinite"})
assert q.sample_threshold == :infinite assert q.sample_threshold == :infinite
end end
test "casts sample_threshold to integer in query struct" do test "casts sample_threshold to integer in query struct", %{site: site} do
q = Query.from(@site, %{"period" => "30d", "sample_threshold" => "30000000"}) q = Query.from(site, %{"period" => "30d", "sample_threshold" => "30000000"})
assert q.sample_threshold == 30_000_000 assert q.sample_threshold == 30_000_000
end end
describe "filters" do describe "filters" do
test "parses goal filter" do test "parses goal filter", %{site: site} do
filters = Jason.encode!(%{"goal" => "Signup"}) 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" assert q.filters["goal"] == "Signup"
end end
test "parses source filter" do test "parses source filter", %{site: site} do
filters = Jason.encode!(%{"source" => "Twitter"}) 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" assert q.filters["source"] == "Twitter"
end 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
end end

View File

@ -1,5 +1,6 @@
defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
use PlausibleWeb.ConnCase use PlausibleWeb.ConnCase
@v4_growth_plan_id "change-me-749342"
describe "GET /api/stats/:domain/custom-prop-values/:prop_key" do describe "GET /api/stats/:domain/custom-prop-values/:prop_key" do
setup [:create_user, :log_in, :create_new_site, :add_imported_data] setup [:create_user, :log_in, :create_new_site, :add_imported_data]
@ -177,6 +178,17 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
} }
] ]
end 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 end
describe "GET /api/stats/:domain/custom-prop-values/:prop_key - with goal filter" do describe "GET /api/stats/:domain/custom-prop-values/:prop_key - with goal filter" do

View File

@ -3,6 +3,7 @@ defmodule PlausibleWeb.Api.StatsController.FunnelsTest do
@user_id 123 @user_id 123
@other_user_id 456 @other_user_id 456
@v4_growth_plan_id "change-me-749342"
@build_funnel_with [ @build_funnel_with [
{"page_path", "/blog/announcement"}, {"page_path", "/blog/announcement"},
@ -219,6 +220,25 @@ defmodule PlausibleWeb.Api.StatsController.FunnelsTest do
] ]
} = resp } = resp
end 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 end
describe "GET /api/stats/funnel - disallowed filters" do describe "GET /api/stats/funnel - disallowed filters" do

View File

@ -1,6 +1,7 @@
defmodule PlausibleWeb.Api.StatsController.TopStatsTest do defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
use PlausibleWeb.ConnCase use PlausibleWeb.ConnCase
@v4_growth_plan_id "change-me-749342"
@user_id 123 @user_id 123
describe "GET /api/stats/top-stats - default" do describe "GET /api/stats/top-stats - default" do
@ -825,6 +826,28 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
refute "Average revenue" in metrics refute "Average revenue" in metrics
refute "Total revenue" in metrics refute "Total revenue" in metrics
end 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 end
describe "GET /api/stats/top-stats - with comparisons" do describe "GET /api/stats/top-stats - with comparisons" do

View File

@ -2,6 +2,7 @@ defmodule PlausibleWeb.StatsControllerTest do
use PlausibleWeb.ConnCase, async: false use PlausibleWeb.ConnCase, async: false
use Plausible.Repo use Plausible.Repo
import Plausible.Test.Support.HTML import Plausible.Test.Support.HTML
@v4_growth_plan_id "change-me-749342"
describe "GET /:website - anonymous user" do describe "GET /:website - anonymous user" do
test "public site - shows site stats", %{conn: conn} do test "public site - shows site stats", %{conn: conn} do
@ -154,6 +155,20 @@ defmodule PlausibleWeb.StatsControllerTest do
assert 'utm_terms.csv' in zip assert 'utm_terms.csv' in zip
end 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 test "exports data in zipped csvs", %{conn: conn, site: site} do
populate_exported_stats(site) populate_exported_stats(site)
conn = get(conn, "/" <> site.domain <> "/export?date=2021-10-20") conn = get(conn, "/" <> site.domain <> "/export?date=2021-10-20")