Allow setting sample_threshold in dashboard API queries (#2958)

* refactor Query.from/2

* allow specifying sample_threshold number in query
This commit is contained in:
RobertJoonas 2023-05-24 14:25:05 +03:00 committed by GitHub
parent 6be67bc6c2
commit dafb60164a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 100 additions and 90 deletions

View File

@ -514,7 +514,7 @@ defmodule Plausible.Stats.Base do
defp add_sample_hint(db_q, query) do defp add_sample_hint(db_q, query) do
case query.sample_threshold do case query.sample_threshold do
"infinite" -> :infinite ->
db_q db_q
threshold -> threshold ->

View File

@ -38,6 +38,34 @@ defmodule Plausible.Stats.Interval do
end end
end end
@doc """
Returns the suggested interval for the given `Date.Range` struct.
## Examples
iex> Plausible.Stats.Interval.default_for_date_range(Date.range(~D[2022-01-01], ~D[2023-01-01]))
"month"
iex> Plausible.Stats.Interval.default_for_date_range(Date.range(~D[2022-01-01], ~D[2022-01-15]))
"date"
iex> Plausible.Stats.Interval.default_for_date_range(Date.range(~D[2022-01-01], ~D[2022-01-01]))
"hour"
"""
def default_for_date_range(%Date.Range{first: first, last: last}) do
cond do
Timex.diff(last, first, :months) > 0 ->
"month"
Timex.diff(last, first, :days) > 0 ->
"date"
true ->
"hour"
end
end
@valid_by_period %{ @valid_by_period %{
"realtime" => ["minute"], "realtime" => ["minute"],
"day" => ["minute", "hour"], "day" => ["minute", "hour"],

View File

@ -13,60 +13,53 @@ defmodule Plausible.Stats.Query do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
def from(site, %{"period" => "realtime"} = params) do def from(site, params) do
query_by_period(site, params)
|> maybe_put_interval(params)
|> put_parsed_filters(params)
|> put_imported_opts(site, params)
|> put_sample_threshold(params)
end
defp query_by_period(site, %{"period" => "realtime"}) do
date = today(site.timezone) date = today(site.timezone)
%__MODULE__{ %__MODULE__{
period: "realtime", period: "realtime",
interval: params["interval"] || Interval.default_for_period(params["period"]), date_range: Date.range(date, date)
date_range: Date.range(date, date),
filters: FilterParser.parse_filters(params["filters"]),
sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold)
} }
end end
def from(site, %{"period" => "day"} = params) do defp query_by_period(site, %{"period" => "day"} = params) do
date = parse_single_date(site.timezone, params) date = parse_single_date(site.timezone, params)
%__MODULE__{ %__MODULE__{
period: "day", period: "day",
date_range: Date.range(date, date), date_range: Date.range(date, date)
interval: params["interval"] || Interval.default_for_period(params["period"]),
filters: FilterParser.parse_filters(params["filters"]),
sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold)
} }
|> put_imported_opts(site, params)
end end
def from(site, %{"period" => "7d"} = params) do defp query_by_period(site, %{"period" => "7d"} = params) do
end_date = parse_single_date(site.timezone, params) end_date = parse_single_date(site.timezone, params)
start_date = end_date |> Timex.shift(days: -6) start_date = end_date |> Timex.shift(days: -6)
%__MODULE__{ %__MODULE__{
period: "7d", period: "7d",
date_range: Date.range(start_date, end_date), date_range: Date.range(start_date, end_date)
interval: params["interval"] || Interval.default_for_period(params["period"]),
filters: FilterParser.parse_filters(params["filters"]),
sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold)
} }
|> put_imported_opts(site, params)
end end
def from(site, %{"period" => "30d"} = params) do defp query_by_period(site, %{"period" => "30d"} = params) do
end_date = parse_single_date(site.timezone, params) end_date = parse_single_date(site.timezone, params)
start_date = end_date |> Timex.shift(days: -30) start_date = end_date |> Timex.shift(days: -30)
%__MODULE__{ %__MODULE__{
period: "30d", period: "30d",
date_range: Date.range(start_date, end_date), date_range: Date.range(start_date, end_date)
interval: params["interval"] || Interval.default_for_period(params["period"]),
filters: FilterParser.parse_filters(params["filters"]),
sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold)
} }
|> put_imported_opts(site, params)
end end
def from(site, %{"period" => "month"} = params) do defp query_by_period(site, %{"period" => "month"} = params) do
date = parse_single_date(site.timezone, params) date = parse_single_date(site.timezone, params)
start_date = Timex.beginning_of_month(date) start_date = Timex.beginning_of_month(date)
@ -74,15 +67,11 @@ defmodule Plausible.Stats.Query do
%__MODULE__{ %__MODULE__{
period: "month", period: "month",
date_range: Date.range(start_date, end_date), date_range: Date.range(start_date, end_date)
interval: params["interval"] || Interval.default_for_period(params["period"]),
filters: FilterParser.parse_filters(params["filters"]),
sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold)
} }
|> put_imported_opts(site, params)
end end
def from(site, %{"period" => "6mo"} = params) do defp query_by_period(site, %{"period" => "6mo"} = params) do
end_date = end_date =
parse_single_date(site.timezone, params) parse_single_date(site.timezone, params)
|> Timex.end_of_month() |> Timex.end_of_month()
@ -93,15 +82,11 @@ defmodule Plausible.Stats.Query do
%__MODULE__{ %__MODULE__{
period: "6mo", period: "6mo",
date_range: Date.range(start_date, end_date), date_range: Date.range(start_date, end_date)
interval: params["interval"] || Interval.default_for_period(params["period"]),
filters: FilterParser.parse_filters(params["filters"]),
sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold)
} }
|> put_imported_opts(site, params)
end end
def from(site, %{"period" => "12mo"} = params) do defp query_by_period(site, %{"period" => "12mo"} = params) do
end_date = end_date =
parse_single_date(site.timezone, params) parse_single_date(site.timezone, params)
|> Timex.end_of_month() |> Timex.end_of_month()
@ -112,15 +97,11 @@ defmodule Plausible.Stats.Query do
%__MODULE__{ %__MODULE__{
period: "12mo", period: "12mo",
date_range: Date.range(start_date, end_date), date_range: Date.range(start_date, end_date)
interval: params["interval"] || Interval.default_for_period(params["period"]),
filters: FilterParser.parse_filters(params["filters"]),
sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold)
} }
|> put_imported_opts(site, params)
end end
def from(site, %{"period" => "year"} = params) do defp query_by_period(site, %{"period" => "year"} = params) do
end_date = end_date =
parse_single_date(site.timezone, params) parse_single_date(site.timezone, params)
|> Timex.end_of_year() |> Timex.end_of_year()
@ -129,76 +110,67 @@ defmodule Plausible.Stats.Query do
%__MODULE__{ %__MODULE__{
period: "year", period: "year",
date_range: Date.range(start_date, end_date), date_range: Date.range(start_date, end_date)
interval: params["interval"] || Interval.default_for_period(params["period"]),
filters: FilterParser.parse_filters(params["filters"]),
sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold)
} }
|> put_imported_opts(site, params)
end end
def from(site, %{"period" => "all"} = params) do defp query_by_period(site, %{"period" => "all"}) do
now = today(site.timezone) now = today(site.timezone)
start_date = Plausible.Site.local_start_date(site) || now start_date = Plausible.Site.local_start_date(site) || now
cond do date_range = Date.range(start_date, now)
Timex.diff(now, start_date, :months) > 0 ->
from(
site,
Map.merge(params, %{
"period" => "custom",
"from" => Date.to_iso8601(start_date),
"to" => Date.to_iso8601(now),
"interval" => params["interval"] || "month"
})
)
|> Map.put(:period, "all")
Timex.diff(now, start_date, :days) > 0 -> %__MODULE__{
from( period: "all",
site, date_range: date_range,
Map.merge(params, %{ interval: Interval.default_for_date_range(date_range)
"period" => "custom", }
"from" => Date.to_iso8601(start_date),
"to" => Date.to_iso8601(now),
"interval" => params["interval"] || "date"
})
)
|> Map.put(:period, "all")
true ->
from(site, Map.merge(params, %{"period" => "day", "date" => "today"}))
|> Map.put(:period, "all")
end
end end
def from(site, %{"period" => "custom", "from" => from, "to" => to} = params) do defp query_by_period(site, %{"period" => "custom", "from" => from, "to" => to} = params) do
new_params = new_params =
params params
|> Map.delete("from") |> Map.drop(["from", "to"])
|> Map.delete("to")
|> Map.put("date", Enum.join([from, to], ",")) |> Map.put("date", Enum.join([from, to], ","))
from(site, new_params) query_by_period(site, new_params)
end end
def from(site, %{"period" => "custom", "date" => date} = params) do defp query_by_period(_site, %{"period" => "custom", "date" => date}) do
[from, to] = String.split(date, ",") [from, to] = String.split(date, ",")
from_date = Date.from_iso8601!(String.trim(from)) from_date = Date.from_iso8601!(String.trim(from))
to_date = Date.from_iso8601!(String.trim(to)) to_date = Date.from_iso8601!(String.trim(to))
%__MODULE__{ %__MODULE__{
period: "custom", period: "custom",
date_range: Date.range(from_date, to_date), date_range: Date.range(from_date, to_date)
interval: params["interval"] || Interval.default_for_period(params["period"]),
filters: FilterParser.parse_filters(params["filters"]),
sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold)
} }
|> put_imported_opts(site, params)
end end
def from(tz, params) do defp query_by_period(site, params) do
__MODULE__.from(tz, Map.merge(params, %{"period" => "30d"})) query_by_period(site, Map.merge(params, %{"period" => "30d"}))
end
defp maybe_put_interval(%{interval: nil} = query, params) do
interval = Map.get(params, "interval", Interval.default_for_period(query.period))
Map.put(query, :interval, interval)
end
defp maybe_put_interval(query, _), do: query
defp put_parsed_filters(query, params) do
Map.put(query, :filters, FilterParser.parse_filters(params["filters"]))
end
defp put_sample_threshold(query, params) do
sample_threshold =
case params["sample_threshold"] do
nil -> @default_sample_threshold
"infinite" -> :infinite
value -> String.to_integer(value)
end
Map.put(query, :sample_threshold, sample_threshold)
end end
def put_filter(query, key, val) do def put_filter(query, key, val) do
@ -244,7 +216,7 @@ defmodule Plausible.Stats.Query do
case params["date"] do case params["date"] do
"today" -> Timex.now(tz) |> Timex.to_date() "today" -> Timex.now(tz) |> Timex.to_date()
date when is_binary(date) -> Date.from_iso8601!(date) date when is_binary(date) -> Date.from_iso8601!(date)
_ -> Timex.now(tz) |> Timex.to_date() _ -> today(tz)
end end
end end

View File

@ -142,6 +142,16 @@ defmodule Plausible.Stats.QueryTest do
assert q.interval == "date" assert q.interval == "date"
end end
test "adds sample_threshold :infinite to query struct" 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"})
assert q.sample_threshold == 30_000_000
end
describe "filters" do describe "filters" do
test "parses goal filter" do test "parses goal filter" do
filters = Jason.encode!(%{"goal" => "Signup"}) filters = Jason.encode!(%{"goal" => "Signup"})