2021-02-05 12:23:30 +03:00
|
|
|
defmodule PlausibleWeb.Api.ExternalStatsController do
|
|
|
|
use PlausibleWeb, :controller
|
|
|
|
use Plausible.Repo
|
|
|
|
use Plug.ErrorHandler
|
|
|
|
alias Plausible.Stats.Query
|
|
|
|
|
|
|
|
def realtime_visitors(conn, _params) do
|
|
|
|
site = conn.assigns[:site]
|
|
|
|
query = Query.from(site.timezone, %{"period" => "realtime"})
|
|
|
|
json(conn, Plausible.Stats.Clickhouse.current_visitors(site, query))
|
|
|
|
end
|
|
|
|
|
|
|
|
def aggregate(conn, params) do
|
2021-02-10 17:27:58 +03:00
|
|
|
with :ok <- validate_date(params),
|
|
|
|
:ok <- validate_period(params),
|
|
|
|
:ok <- validate_metrics(params) do
|
|
|
|
site = conn.assigns[:site]
|
|
|
|
query = Query.from(site.timezone, params)
|
|
|
|
|
|
|
|
metrics =
|
|
|
|
params["metrics"]
|
|
|
|
|> String.split(",")
|
|
|
|
|> Enum.map(&String.trim/1)
|
|
|
|
|
|
|
|
result =
|
|
|
|
if params["compare"] == "previous_period" do
|
|
|
|
prev_query = Query.shift_back(query)
|
|
|
|
|
|
|
|
[prev_result, curr_result] =
|
|
|
|
Task.await_many([
|
|
|
|
Task.async(fn -> Plausible.Stats.aggregate(site, prev_query, metrics) end),
|
|
|
|
Task.async(fn -> Plausible.Stats.aggregate(site, query, metrics) end)
|
|
|
|
])
|
|
|
|
|
|
|
|
Enum.map(curr_result, fn {metric, %{value: current_val}} ->
|
|
|
|
%{value: prev_val} = prev_result[metric]
|
|
|
|
|
|
|
|
{metric,
|
|
|
|
%{
|
|
|
|
value: current_val,
|
|
|
|
change: percent_change(prev_val, current_val)
|
|
|
|
}}
|
|
|
|
end)
|
|
|
|
|> Enum.into(%{})
|
|
|
|
else
|
|
|
|
Plausible.Stats.aggregate(site, query, metrics)
|
|
|
|
end
|
|
|
|
|
|
|
|
json(conn, result)
|
|
|
|
else
|
|
|
|
{:error, msg} ->
|
|
|
|
conn
|
|
|
|
|> put_status(400)
|
|
|
|
|> json(%{error: msg})
|
|
|
|
end
|
2021-02-05 12:23:30 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
def timeseries(conn, params) do
|
2021-02-10 17:27:58 +03:00
|
|
|
with :ok <- validate_date(params),
|
|
|
|
:ok <- validate_period(params),
|
|
|
|
:ok <- validate_interval(params) do
|
|
|
|
site = conn.assigns[:site]
|
|
|
|
query = Query.from(site.timezone, params)
|
|
|
|
|
|
|
|
{plot, labels} = Plausible.Stats.timeseries(site, query)
|
|
|
|
|
|
|
|
graph =
|
|
|
|
Enum.zip(labels, plot)
|
|
|
|
|> Enum.map(fn {label, val} -> %{date: label, value: val} end)
|
|
|
|
|> Enum.into([])
|
|
|
|
|
|
|
|
json(conn, graph)
|
|
|
|
else
|
|
|
|
{:error, msg} ->
|
|
|
|
conn
|
|
|
|
|> put_status(400)
|
|
|
|
|> json(%{error: msg})
|
|
|
|
end
|
2021-02-05 12:23:30 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
def handle_errors(conn, %{kind: kind, reason: reason}) do
|
|
|
|
json(conn, %{error: Exception.format_banner(kind, reason)})
|
|
|
|
end
|
|
|
|
|
|
|
|
defp percent_change(old_count, new_count) do
|
|
|
|
cond do
|
|
|
|
old_count == 0 and new_count > 0 ->
|
|
|
|
100
|
|
|
|
|
|
|
|
old_count == 0 and new_count == 0 ->
|
|
|
|
0
|
|
|
|
|
|
|
|
true ->
|
|
|
|
round((new_count - old_count) / old_count * 100)
|
|
|
|
end
|
|
|
|
end
|
2021-02-10 17:27:58 +03:00
|
|
|
|
|
|
|
defp validate_date(%{"date" => date}) do
|
|
|
|
case Date.from_iso8601(date) do
|
|
|
|
{:ok, _date} ->
|
|
|
|
:ok
|
|
|
|
|
|
|
|
{:error, msg} ->
|
|
|
|
{:error,
|
|
|
|
"Error parsing `date` parameter: #{msg}. Please specify a valid date in ISO-8601 format."}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp validate_date(_), do: :ok
|
|
|
|
|
|
|
|
defp validate_period(%{"period" => period}) do
|
|
|
|
if period in ["day", "7d", "30d", "month", "6mo", "12mo", "custom"] do
|
|
|
|
:ok
|
|
|
|
else
|
|
|
|
{:error,
|
|
|
|
"Error parsing `period` parameter: invalid period `#{period}`. Please find accepted values in our docs: https://plausible.io/docs/stats-api#time-periods"}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp validate_period(_), do: :ok
|
|
|
|
|
|
|
|
@valid_metrics ["pageviews", "visitors", "bounce_rate", "visit_duration"]
|
|
|
|
@valid_metrics_str Enum.map(@valid_metrics, &("`" <> &1 <> "`")) |> Enum.join(", ")
|
|
|
|
|
|
|
|
defp validate_metrics(%{"metrics" => metrics_str}) do
|
|
|
|
metrics =
|
|
|
|
metrics_str
|
|
|
|
|> String.split(",")
|
|
|
|
|> Enum.map(&String.trim/1)
|
|
|
|
|
|
|
|
bad_metric = Enum.find(metrics, fn metric -> metric not in @valid_metrics end)
|
|
|
|
|
|
|
|
if bad_metric do
|
|
|
|
{:error,
|
|
|
|
"Error parsing `metrics` parameter: invalid metric `#{bad_metric}`. Valid metrics are #{
|
|
|
|
@valid_metrics_str
|
|
|
|
}"}
|
|
|
|
else
|
|
|
|
:ok
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp validate_metrics(_), do: :ok
|
|
|
|
|
|
|
|
@valid_intervals ["date", "month"]
|
|
|
|
@valid_intervals_str Enum.map(@valid_intervals, &("`" <> &1 <> "`")) |> Enum.join(", ")
|
|
|
|
|
|
|
|
defp validate_interval(%{"interval" => interval}) do
|
|
|
|
if interval in @valid_intervals do
|
|
|
|
:ok
|
|
|
|
else
|
|
|
|
{:error,
|
|
|
|
"Error parsing `interval` parameter: invalid interval `#{interval}`. Valid intervals are #{
|
|
|
|
@valid_intervals_str
|
|
|
|
}"}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp validate_interval(_), do: :ok
|
2021-02-05 12:23:30 +03:00
|
|
|
end
|