mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 01:22:15 +03:00
APIv2: Revenue metrics (#4659)
* WIP: Start refactoring revenue metrics * Hacks to make things work * Remove old revenue code, remove revenue metrics if needed * Update query_optimizer docs * Minor fixes * Add tests around average/total revenue when non-revenue goal filtering going on * Optimize, calculate filters as expected (OR-ing clauses) * Revenue: Handle cases where revenue metrics should not be returned or nil * Expose revenue metrics in internal schema, add tests * Docstring * Remove TODO * Typegen * Solve warnings * Remove nesting * ce_test fix * Tag tests as ee_only * Fix: When filtering by revenue goal and no conversions, return 0.0 instead of nil * More straight-forward preloading logic
This commit is contained in:
parent
5fec52ab36
commit
141eea88ff
6
assets/js/types/query-api.d.ts
vendored
6
assets/js/types/query-api.d.ts
vendored
@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
export type Metric =
|
||||
| "time_on_page"
|
||||
| "visitors"
|
||||
| "visits"
|
||||
| "pageviews"
|
||||
@ -16,7 +15,10 @@ export type Metric =
|
||||
| "events"
|
||||
| "percentage"
|
||||
| "conversion_rate"
|
||||
| "group_conversion_rate";
|
||||
| "group_conversion_rate"
|
||||
| "time_on_page"
|
||||
| "total_revenue"
|
||||
| "average_revenue";
|
||||
export type DateRangeShorthand = "30m" | "realtime" | "all" | "day" | "7d" | "30d" | "month" | "6mo" | "12mo" | "year";
|
||||
/**
|
||||
* @minItems 2
|
||||
|
@ -2,9 +2,6 @@ defmodule Plausible.Stats.Goal.Revenue do
|
||||
@moduledoc """
|
||||
Revenue specific functions for the stats scope
|
||||
"""
|
||||
import Ecto.Query
|
||||
|
||||
alias Plausible.Stats.Filters
|
||||
|
||||
@revenue_metrics [:average_revenue, :total_revenue]
|
||||
|
||||
@ -12,73 +9,65 @@ defmodule Plausible.Stats.Goal.Revenue do
|
||||
@revenue_metrics
|
||||
end
|
||||
|
||||
@spec get_revenue_tracking_currency(Plausible.Site.t(), Plausible.Stats.Query.t(), [atom()]) ::
|
||||
{atom() | nil, [atom()]}
|
||||
@doc """
|
||||
Returns the common currency for the goal filters in a query. If there are no
|
||||
goal filters, multiple currencies or the site owner does not have access to
|
||||
revenue goals, `nil` is returned and revenue metrics are dropped.
|
||||
Preloads revenue currencies for a query.
|
||||
|
||||
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
|
||||
EUR, revenue metrics are dropped.
|
||||
Assumptions and business logic:
|
||||
1. Goals are already filtered according to query filters and dimensions
|
||||
2. If there's a single currency involved, return map containing the default
|
||||
3. If there's a breakdown by event:goal we return all the relevant currencies as a map
|
||||
4. If filtering by multiple different currencies without event:goal breakdown empty map is returned
|
||||
5. If user has no access or preloading is not needed, empty map is returned
|
||||
|
||||
The resulting data structure is attached to a `Query` and used below in `format_revenue_metric/3`.
|
||||
"""
|
||||
def get_revenue_tracking_currency(site, query, metrics) do
|
||||
goal_filters =
|
||||
case Filters.get_toplevel_filter(query, "event:goal") do
|
||||
[:is, "event:goal", list] -> list
|
||||
_ -> []
|
||||
def preload_revenue_currencies(site, goals, metrics, dimensions) do
|
||||
if requested?(metrics) and length(goals) > 0 and available?(site) do
|
||||
goal_currency_map =
|
||||
goals
|
||||
|> Map.new(fn goal -> {Plausible.Goal.display_name(goal), goal.currency} end)
|
||||
|> Map.reject(fn {_goal, currency} -> is_nil(currency) end)
|
||||
|
||||
currencies = goal_currency_map |> Map.values() |> Enum.uniq()
|
||||
goal_dimension? = "event:goal" in dimensions
|
||||
|
||||
case {currencies, goal_dimension?} do
|
||||
{[currency], false} -> %{default: currency}
|
||||
{_, true} -> goal_currency_map
|
||||
_ -> %{}
|
||||
end
|
||||
|
||||
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),
|
||||
where: rg.display_name in ^goal_filters,
|
||||
select: rg.currency,
|
||||
distinct: true
|
||||
)
|
||||
|
||||
if length(revenue_goals_currencies) == 1,
|
||||
do: {List.first(revenue_goals_currencies), metrics},
|
||||
else: {nil, metrics -- @revenue_metrics}
|
||||
else
|
||||
{nil, metrics -- @revenue_metrics}
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
def cast_revenue_metrics_to_money([%{goal: _goal} | _rest] = results, revenue_goals)
|
||||
when is_list(revenue_goals) do
|
||||
for result <- results do
|
||||
if matching_goal = Enum.find(revenue_goals, &(&1.display_name == result.goal)) do
|
||||
cast_revenue_metrics_to_money(result, matching_goal.currency)
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
def format_revenue_metric(value, query, dimension_values) do
|
||||
currency =
|
||||
query.revenue_currencies[:default] ||
|
||||
get_goal_dimension_revenue_currency(query, dimension_values)
|
||||
|
||||
def cast_revenue_metrics_to_money(results, currency) when is_map(results) do
|
||||
for {metric, value} <- results, into: %{} do
|
||||
{metric, maybe_cast_metric_to_money(value, metric, currency)}
|
||||
end
|
||||
end
|
||||
|
||||
def cast_revenue_metrics_to_money(results, _), do: results
|
||||
|
||||
def maybe_cast_metric_to_money(value, metric, currency) do
|
||||
if currency && metric in @revenue_metrics do
|
||||
if currency do
|
||||
Money.new!(value || 0, currency)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def available?(site) do
|
||||
site = Plausible.Repo.preload(site, :owner)
|
||||
Plausible.Billing.Feature.RevenueGoals.check_availability(site.owner) == :ok
|
||||
end
|
||||
|
||||
# :NOTE: Legacy queries don't have metrics associated with them so work around the issue by assuming
|
||||
# revenue metric was requested.
|
||||
def requested?([]), do: true
|
||||
def requested?(metrics), do: Enum.any?(metrics, &(&1 in @revenue_metrics))
|
||||
|
||||
defp get_goal_dimension_revenue_currency(query, dimension_values) do
|
||||
Enum.zip(query.dimensions, dimension_values)
|
||||
|> Enum.find_value(fn
|
||||
{"event:goal", goal_label} -> Map.get(query.revenue_currencies, goal_label)
|
||||
_ -> nil
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
@ -34,18 +34,39 @@ defmodule Plausible.Goals.Filters do
|
||||
end)
|
||||
end
|
||||
|
||||
def filter_preloaded(preloaded_goals, operation, clause) when operation in [:is, :contains] do
|
||||
Enum.filter(preloaded_goals, fn goal ->
|
||||
case operation do
|
||||
:is ->
|
||||
Plausible.Goal.display_name(goal) == clause
|
||||
def preload_needed_goals(site, filters) do
|
||||
goals = Plausible.Goals.for_site(site)
|
||||
|
||||
:contains ->
|
||||
String.contains?(Plausible.Goal.display_name(goal), clause)
|
||||
end
|
||||
Enum.reduce(filters, goals, fn
|
||||
[operation, "event:goal", clauses], goals ->
|
||||
goals_matching_any_clause(goals, operation, clauses)
|
||||
|
||||
_filter, goals ->
|
||||
goals
|
||||
end)
|
||||
end
|
||||
|
||||
def filter_preloaded(preloaded_goals, operation, clause) when operation in [:is, :contains] do
|
||||
Enum.filter(preloaded_goals, fn goal -> matches?(goal, operation, clause) end)
|
||||
end
|
||||
|
||||
defp goals_matching_any_clause(goals, operation, clauses) do
|
||||
goals
|
||||
|> Enum.filter(fn goal ->
|
||||
Enum.any?(clauses, fn clause -> matches?(goal, operation, clause) end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp matches?(goal, operation, clause) do
|
||||
case operation do
|
||||
:is ->
|
||||
Plausible.Goal.display_name(goal) == clause
|
||||
|
||||
:contains ->
|
||||
String.contains?(Plausible.Goal.display_name(goal), clause)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_condition(filtered_goals, imported?) do
|
||||
Enum.reduce(filtered_goals, false, fn goal, dynamic_statement ->
|
||||
case goal do
|
||||
|
@ -6,20 +6,16 @@ defmodule Plausible.Stats.Aggregate do
|
||||
"""
|
||||
|
||||
use Plausible.ClickhouseRepo
|
||||
use Plausible
|
||||
alias Plausible.Stats.{Query, QueryRunner}
|
||||
alias Plausible.Stats.{Query, QueryRunner, QueryOptimizer}
|
||||
|
||||
def aggregate(site, query, metrics) do
|
||||
{currency, metrics} =
|
||||
on_ee do
|
||||
Plausible.Stats.Goal.Revenue.get_revenue_tracking_currency(site, query, metrics)
|
||||
else
|
||||
{nil, metrics}
|
||||
end
|
||||
|
||||
Query.trace(query, metrics)
|
||||
|
||||
query = %Query{query | metrics: metrics}
|
||||
query =
|
||||
query
|
||||
|> Query.set(metrics: metrics, remove_unavailable_revenue_metrics: true)
|
||||
|> QueryOptimizer.optimize()
|
||||
|
||||
query_result = QueryRunner.run(site, query)
|
||||
|
||||
[entry] = query_result.results
|
||||
@ -29,7 +25,7 @@ defmodule Plausible.Stats.Aggregate do
|
||||
|> Enum.map(fn {metric, index} ->
|
||||
{
|
||||
metric,
|
||||
metric_map(entry, index, metric, currency)
|
||||
metric_map(entry, index, metric)
|
||||
}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
@ -38,27 +34,25 @@ defmodule Plausible.Stats.Aggregate do
|
||||
def metric_map(
|
||||
%{metrics: metrics, comparison: %{metrics: comparison_metrics, change: change}},
|
||||
index,
|
||||
metric,
|
||||
currency
|
||||
metric
|
||||
) do
|
||||
%{
|
||||
value: get_value(metrics, index, metric, currency),
|
||||
comparison_value: get_value(comparison_metrics, index, metric, currency),
|
||||
value: get_value(metrics, index, metric),
|
||||
comparison_value: get_value(comparison_metrics, index, metric),
|
||||
change: Enum.at(change, index)
|
||||
}
|
||||
end
|
||||
|
||||
def metric_map(%{metrics: metrics}, index, metric, currency) do
|
||||
def metric_map(%{metrics: metrics}, index, metric) do
|
||||
%{
|
||||
value: get_value(metrics, index, metric, currency)
|
||||
value: get_value(metrics, index, metric)
|
||||
}
|
||||
end
|
||||
|
||||
def get_value(metric_list, index, metric, currency) do
|
||||
def get_value(metric_list, index, metric) do
|
||||
metric_list
|
||||
|> Enum.at(index)
|
||||
|> maybe_round_value(metric)
|
||||
|> maybe_cast_metric_to_money(metric, currency)
|
||||
end
|
||||
|
||||
@metrics_to_round [:bounce_rate, :time_on_page, :visit_duration, :sample_percent]
|
||||
@ -66,12 +60,4 @@ defmodule Plausible.Stats.Aggregate do
|
||||
defp maybe_round_value(nil, _metric), do: nil
|
||||
defp maybe_round_value(value, metric) when metric in @metrics_to_round, do: round(value)
|
||||
defp maybe_round_value(value, _metric), do: value
|
||||
|
||||
on_ee do
|
||||
defp maybe_cast_metric_to_money(value, metric, currency) do
|
||||
Plausible.Stats.Goal.Revenue.maybe_cast_metric_to_money(value, metric, currency)
|
||||
end
|
||||
else
|
||||
defp maybe_cast_metric_to_money(value, _metric, _currency), do: value
|
||||
end
|
||||
end
|
||||
|
@ -6,10 +6,9 @@ defmodule Plausible.Stats.Breakdown do
|
||||
"""
|
||||
|
||||
use Plausible.ClickhouseRepo
|
||||
use Plausible
|
||||
use Plausible.Stats.SQL.Fragments
|
||||
|
||||
alias Plausible.Stats.{Query, QueryOptimizer, QueryRunner}
|
||||
alias Plausible.Stats.{Query, QueryRunner, QueryOptimizer}
|
||||
|
||||
def breakdown(
|
||||
site,
|
||||
@ -22,8 +21,8 @@ defmodule Plausible.Stats.Breakdown do
|
||||
transformed_order_by = transform_order_by(order_by || [], dimension)
|
||||
|
||||
query_with_metrics =
|
||||
Query.set(
|
||||
query,
|
||||
query
|
||||
|> Query.set(
|
||||
metrics: transformed_metrics,
|
||||
# Concat client requested order with default order, overriding only if client explicitly requests it
|
||||
order_by:
|
||||
@ -34,13 +33,13 @@ defmodule Plausible.Stats.Breakdown do
|
||||
pagination: %{limit: limit, offset: (page - 1) * limit},
|
||||
v2: true,
|
||||
# Allow pageview and event metrics to be queried off of sessions table
|
||||
legacy_breakdown: true
|
||||
legacy_breakdown: true,
|
||||
remove_unavailable_revenue_metrics: true
|
||||
)
|
||||
|> QueryOptimizer.optimize()
|
||||
|
||||
QueryRunner.run(site, query_with_metrics)
|
||||
|> build_breakdown_result(query_with_metrics, metrics)
|
||||
|> update_currency_metrics(site, query_with_metrics)
|
||||
end
|
||||
|
||||
defp build_breakdown_result(query_result, query, metrics) do
|
||||
@ -122,41 +121,4 @@ defmodule Plausible.Stats.Breakdown do
|
||||
end
|
||||
|
||||
defp dimension_filters(_), do: []
|
||||
|
||||
on_ee do
|
||||
defp update_currency_metrics(results, site, %Query{dimensions: ["event:goal"]}) do
|
||||
site = Plausible.Repo.preload(site, :goals)
|
||||
|
||||
{event_goals, _pageview_goals} = Enum.split_with(site.goals, & &1.event_name)
|
||||
revenue_goals = Enum.filter(event_goals, &Plausible.Goal.Revenue.revenue?/1)
|
||||
|
||||
if length(revenue_goals) > 0 and Plausible.Billing.Feature.RevenueGoals.enabled?(site) do
|
||||
Plausible.Stats.Goal.Revenue.cast_revenue_metrics_to_money(results, revenue_goals)
|
||||
else
|
||||
remove_revenue_metrics(results)
|
||||
end
|
||||
end
|
||||
|
||||
defp update_currency_metrics(results, site, query) do
|
||||
{currency, _metrics} =
|
||||
Plausible.Stats.Goal.Revenue.get_revenue_tracking_currency(site, query, query.metrics)
|
||||
|
||||
if currency do
|
||||
results
|
||||
|> Enum.map(&Plausible.Stats.Goal.Revenue.cast_revenue_metrics_to_money(&1, currency))
|
||||
else
|
||||
remove_revenue_metrics(results)
|
||||
end
|
||||
end
|
||||
else
|
||||
defp update_currency_metrics(results, _site, _query), do: remove_revenue_metrics(results)
|
||||
end
|
||||
|
||||
defp remove_revenue_metrics(results) do
|
||||
Enum.map(results, fn map ->
|
||||
map
|
||||
|> Map.delete(:total_revenue)
|
||||
|> Map.delete(:average_revenue)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,8 @@
|
||||
defmodule Plausible.Stats.Filters.QueryParser do
|
||||
@moduledoc false
|
||||
|
||||
use Plausible
|
||||
|
||||
alias Plausible.Stats.{TableDecider, Filters, Metrics, DateTimeRange, JSONSchema}
|
||||
|
||||
@default_include %{
|
||||
@ -36,7 +38,8 @@ defmodule Plausible.Stats.Filters.QueryParser do
|
||||
{:ok, order_by} <- parse_order_by(Map.get(params, "order_by")),
|
||||
{:ok, include} <- parse_include(site, Map.get(params, "include", %{})),
|
||||
{:ok, pagination} <- parse_pagination(Map.get(params, "pagination", %{})),
|
||||
preloaded_goals <- preload_goals_if_needed(site, filters, dimensions),
|
||||
{preloaded_goals, revenue_currencies} <-
|
||||
preload_needed_goals(site, metrics, filters, dimensions),
|
||||
query = %{
|
||||
metrics: metrics,
|
||||
filters: filters,
|
||||
@ -44,15 +47,17 @@ defmodule Plausible.Stats.Filters.QueryParser do
|
||||
dimensions: dimensions,
|
||||
order_by: order_by,
|
||||
timezone: site.timezone,
|
||||
preloaded_goals: preloaded_goals,
|
||||
include: include,
|
||||
pagination: pagination
|
||||
pagination: pagination,
|
||||
preloaded_goals: preloaded_goals,
|
||||
revenue_currencies: revenue_currencies
|
||||
},
|
||||
:ok <- validate_order_by(query),
|
||||
:ok <- validate_custom_props_access(site, query),
|
||||
:ok <- validate_toplevel_only_filter_dimension(query),
|
||||
:ok <- validate_special_metrics_filters(query),
|
||||
:ok <- validate_filtered_goals_exist(query),
|
||||
:ok <- validate_revenue_metrics_access(site, query),
|
||||
:ok <- validate_metrics(query),
|
||||
:ok <- validate_include(query) do
|
||||
{:ok, query}
|
||||
@ -405,14 +410,19 @@ defmodule Plausible.Stats.Filters.QueryParser do
|
||||
end
|
||||
end
|
||||
|
||||
def preload_goals_if_needed(site, filters, dimensions) do
|
||||
def preload_needed_goals(site, metrics, filters, dimensions) do
|
||||
goal_filters? =
|
||||
Enum.any?(filters, fn [_, filter_key | _rest] -> filter_key == "event:goal" end)
|
||||
|
||||
if goal_filters? or Enum.member?(dimensions, "event:goal") do
|
||||
Plausible.Goals.for_site(site)
|
||||
goals = Plausible.Goals.Filters.preload_needed_goals(site, filters)
|
||||
|
||||
{
|
||||
goals,
|
||||
preload_revenue_currencies(site, goals, metrics, dimensions)
|
||||
}
|
||||
else
|
||||
[]
|
||||
{[], %{}}
|
||||
end
|
||||
end
|
||||
|
||||
@ -460,6 +470,25 @@ defmodule Plausible.Stats.Filters.QueryParser do
|
||||
end
|
||||
end
|
||||
|
||||
on_ee do
|
||||
alias Plausible.Stats.Goal.Revenue
|
||||
|
||||
defdelegate preload_revenue_currencies(site, preloaded_goals, metrics, dimensions),
|
||||
to: Plausible.Stats.Goal.Revenue
|
||||
|
||||
defp validate_revenue_metrics_access(site, query) do
|
||||
if Revenue.requested?(query.metrics) and not Revenue.available?(site) do
|
||||
{:error, "The owner of this site does not have access to the revenue metrics feature."}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
else
|
||||
defp preload_revenue_currencies(_site, _preloaded_goals, _metrics, _dimensions), do: %{}
|
||||
|
||||
defp validate_revenue_metrics_access(_site, _query), do: :ok
|
||||
end
|
||||
|
||||
defp validate_goal_filter(clause, configured_goals) do
|
||||
configured_goal_names =
|
||||
Enum.map(configured_goals, fn goal -> Plausible.Goal.display_name(goal) end)
|
||||
|
@ -32,14 +32,15 @@ defmodule Plausible.Stats.Legacy.QueryBuilder do
|
||||
end
|
||||
|
||||
defp put_preloaded_goals(query, site) do
|
||||
goals =
|
||||
Plausible.Stats.Filters.QueryParser.preload_goals_if_needed(
|
||||
{preloaded_goals, revenue_currencies} =
|
||||
Plausible.Stats.Filters.QueryParser.preload_needed_goals(
|
||||
site,
|
||||
query.metrics,
|
||||
query.filters,
|
||||
query.dimensions
|
||||
)
|
||||
|
||||
struct!(query, preloaded_goals: goals)
|
||||
struct!(query, preloaded_goals: preloaded_goals, revenue_currencies: revenue_currencies)
|
||||
end
|
||||
|
||||
defp put_period(%Query{now: now} = query, _site, %{"period" => period})
|
||||
|
@ -17,7 +17,9 @@ defmodule Plausible.Stats.Query do
|
||||
timezone: nil,
|
||||
v2: false,
|
||||
legacy_breakdown: false,
|
||||
remove_unavailable_revenue_metrics: false,
|
||||
preloaded_goals: [],
|
||||
revenue_currencies: %{},
|
||||
include: Plausible.Stats.Filters.QueryParser.default_include(),
|
||||
debug_metadata: %{},
|
||||
pagination: nil
|
||||
|
@ -13,6 +13,8 @@ defmodule Plausible.Stats.QueryOptimizer do
|
||||
1. Figures out what the right granularity to group by time is
|
||||
2. Adds a missing order_by clause to a query
|
||||
3. Updating "time" dimension in order_by to the right granularity
|
||||
4. Updates event:hostname filters to also apply on visit level for sane results.
|
||||
5. Removes revenue metrics from dashboard queries if not requested, present or unavailable for the site.
|
||||
|
||||
"""
|
||||
def optimize(query) do
|
||||
@ -47,7 +49,8 @@ defmodule Plausible.Stats.QueryOptimizer do
|
||||
&update_group_by_time/1,
|
||||
&add_missing_order_by/1,
|
||||
&update_time_in_order_by/1,
|
||||
&extend_hostname_filters_to_visit/1
|
||||
&extend_hostname_filters_to_visit/1,
|
||||
&remove_revenue_metrics_if_unavailable/1
|
||||
]
|
||||
end
|
||||
|
||||
@ -172,4 +175,16 @@ defmodule Plausible.Stats.QueryOptimizer do
|
||||
pagination: nil
|
||||
)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
defp remove_revenue_metrics_if_unavailable(query) do
|
||||
if query.remove_unavailable_revenue_metrics and map_size(query.revenue_currencies) == 0 do
|
||||
Query.set(query, metrics: query.metrics -- Plausible.Stats.Goal.Revenue.revenue_metrics())
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
else
|
||||
defp remove_revenue_metrics_if_unavailable(query), do: query
|
||||
end
|
||||
end
|
||||
|
@ -9,6 +9,7 @@ defmodule Plausible.Stats.QueryRunner do
|
||||
3. Passing total_rows from clickhouse to QueryResult meta
|
||||
"""
|
||||
|
||||
use Plausible
|
||||
use Plausible.ClickhouseRepo
|
||||
|
||||
alias Plausible.Stats.{
|
||||
@ -141,7 +142,7 @@ defmodule Plausible.Stats.QueryRunner do
|
||||
|
||||
%{
|
||||
dimensions: dimensions,
|
||||
metrics: Enum.map(query.metrics, &get_metric(entry, &1, dimensions, time_on_page))
|
||||
metrics: Enum.map(query.metrics, &get_metric(entry, &1, dimensions, query, time_on_page))
|
||||
}
|
||||
end)
|
||||
end
|
||||
@ -168,10 +169,19 @@ defmodule Plausible.Stats.QueryRunner do
|
||||
Map.get(entry, Util.shortname(query, dimension))
|
||||
end
|
||||
|
||||
defp get_metric(_entry, :time_on_page, dimensions, time_on_page),
|
||||
on_ee do
|
||||
defp get_metric(entry, metric, dimensions, query, _time_on_page)
|
||||
when metric in [:average_revenue, :total_revenue] do
|
||||
value = Map.get(entry, metric)
|
||||
|
||||
Plausible.Stats.Goal.Revenue.format_revenue_metric(value, query, dimensions)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_metric(_entry, :time_on_page, dimensions, _query, time_on_page),
|
||||
do: Map.get(time_on_page, dimensions)
|
||||
|
||||
defp get_metric(entry, metric, _dimensions, _time_on_page), do: Map.get(entry, metric)
|
||||
defp get_metric(entry, metric, _dimensions, _query, _time_on_page), do: Map.get(entry, metric)
|
||||
|
||||
# Special case: If comparison and single time dimension, add 0 rows - otherwise
|
||||
# comparisons would not be shown for timeseries with 0 values.
|
||||
|
@ -5,9 +5,8 @@ defmodule Plausible.Stats.Timeseries do
|
||||
Avoid adding new logic here - update QueryBuilder etc instead.
|
||||
"""
|
||||
|
||||
use Plausible
|
||||
use Plausible.ClickhouseRepo
|
||||
alias Plausible.Stats.{Comparisons, Query, QueryRunner, Time}
|
||||
alias Plausible.Stats.{Comparisons, Query, QueryRunner, QueryOptimizer, Time}
|
||||
|
||||
@time_dimension %{
|
||||
"month" => "time:month",
|
||||
@ -18,21 +17,16 @@ defmodule Plausible.Stats.Timeseries do
|
||||
}
|
||||
|
||||
def timeseries(site, query, metrics) do
|
||||
{currency, metrics} =
|
||||
on_ee do
|
||||
Plausible.Stats.Goal.Revenue.get_revenue_tracking_currency(site, query, metrics)
|
||||
else
|
||||
{nil, metrics}
|
||||
end
|
||||
|
||||
query =
|
||||
Query.set(
|
||||
query,
|
||||
query
|
||||
|> Query.set(
|
||||
metrics: transform_metrics(metrics, %{conversion_rate: :group_conversion_rate}),
|
||||
dimensions: [time_dimension(query)],
|
||||
order_by: [{time_dimension(query), :asc}],
|
||||
v2: true
|
||||
v2: true,
|
||||
remove_unavailable_revenue_metrics: true
|
||||
)
|
||||
|> QueryOptimizer.optimize()
|
||||
|
||||
comparison_query =
|
||||
if(query.include.comparisons,
|
||||
@ -43,8 +37,8 @@ defmodule Plausible.Stats.Timeseries do
|
||||
query_result = QueryRunner.run(site, query)
|
||||
|
||||
{
|
||||
build_result(query_result, query, currency, fn entry -> entry end),
|
||||
build_result(query_result, comparison_query, currency, fn entry -> entry.comparison end),
|
||||
build_result(query_result, query, fn entry -> entry end),
|
||||
build_result(query_result, comparison_query, fn entry -> entry.comparison end),
|
||||
query_result.meta
|
||||
}
|
||||
end
|
||||
@ -53,7 +47,7 @@ defmodule Plausible.Stats.Timeseries do
|
||||
|
||||
# Given a query result, build a legacy timeseries result
|
||||
# Format is %{ date => %{ date: date_string, [metric] => value } } with a bunch of special cases for the UI
|
||||
defp build_result(query_result, %Query{} = query, currency, extract_entry) do
|
||||
defp build_result(query_result, %Query{} = query, extract_entry) do
|
||||
query_result.results
|
||||
|> Enum.map(&extract_entry.(&1))
|
||||
|> Enum.map(fn %{dimensions: [time_dimension_value], metrics: metrics} ->
|
||||
@ -65,12 +59,12 @@ defmodule Plausible.Stats.Timeseries do
|
||||
}
|
||||
end)
|
||||
|> Map.new()
|
||||
|> add_labels(query, currency)
|
||||
|> add_labels(query)
|
||||
end
|
||||
|
||||
defp build_result(_, _, _, _), do: nil
|
||||
defp build_result(_, _, _), do: nil
|
||||
|
||||
defp add_labels(results_map, query, currency) do
|
||||
defp add_labels(results_map, query) do
|
||||
query
|
||||
|> Time.time_labels()
|
||||
|> Enum.map(fn key ->
|
||||
@ -79,7 +73,6 @@ defmodule Plausible.Stats.Timeseries do
|
||||
key,
|
||||
empty_row(key, query.metrics)
|
||||
)
|
||||
|> cast_revenue_metrics_to_money(currency)
|
||||
end)
|
||||
|> transform_realtime_labels(query)
|
||||
|> transform_keys(%{group_conversion_rate: :conversion_rate})
|
||||
@ -122,12 +115,4 @@ defmodule Plausible.Stats.Timeseries do
|
||||
end
|
||||
|
||||
defp transform_realtime_labels(results, _query), do: results
|
||||
|
||||
on_ee do
|
||||
defp cast_revenue_metrics_to_money(results, revenue_goals) do
|
||||
Plausible.Stats.Goal.Revenue.cast_revenue_metrics_to_money(results, revenue_goals)
|
||||
end
|
||||
else
|
||||
defp cast_revenue_metrics_to_money(results, _revenue_goals), do: results
|
||||
end
|
||||
end
|
||||
|
@ -218,10 +218,6 @@
|
||||
},
|
||||
"metric": {
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "time_on_page",
|
||||
"$comment": "only :internal"
|
||||
},
|
||||
{
|
||||
"const": "visitors",
|
||||
"description": "Metric counting the number of unique visitors"
|
||||
@ -261,6 +257,18 @@
|
||||
{
|
||||
"const": "group_conversion_rate",
|
||||
"markdownDescription": "The percentage of visitors who completed the goal with the same dimension. Requires: dimension list passed, an `event:goal` filter or `event:goal` dimension"
|
||||
},
|
||||
{
|
||||
"const": "time_on_page",
|
||||
"$comment": "only :internal"
|
||||
},
|
||||
{
|
||||
"const": "total_revenue",
|
||||
"$comment": "only :internal"
|
||||
},
|
||||
{
|
||||
"const": "average_revenue",
|
||||
"$comment": "only :internal"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -47,7 +47,12 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
|
||||
def check_success(params, site, expected_result, schema_type \\ :public) do
|
||||
assert {:ok, result} = parse(site, schema_type, params, @now)
|
||||
|
||||
return_value = Map.take(result, [:preloaded_goals, :revenue_currencies])
|
||||
result = Map.drop(result, [:preloaded_goals, :revenue_currencies])
|
||||
assert result == expected_result
|
||||
|
||||
return_value
|
||||
end
|
||||
|
||||
def check_error(params, site, expected_error_message, schema_type \\ :public) do
|
||||
@ -69,13 +74,22 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
}
|
||||
|
||||
check_success(params, site, expected_parsed, schema_type)
|
||||
end
|
||||
|
||||
def check_goals(actual, opts) do
|
||||
preloaded_goal_names =
|
||||
actual[:preloaded_goals]
|
||||
|> Enum.map(& &1.display_name)
|
||||
|> Enum.sort()
|
||||
|
||||
assert preloaded_goal_names == Keyword.get(opts, :preloaded_goals)
|
||||
assert actual[:revenue_currencies] == Keyword.get(opts, :revenue_currencies)
|
||||
end
|
||||
|
||||
test "parsing empty map fails", %{site: site} do
|
||||
%{}
|
||||
|> check_error(site, "#: Required properties site_id, metrics, date_range were not present.")
|
||||
@ -92,8 +106,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -134,8 +147,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
@ -201,8 +213,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
@ -327,8 +338,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -353,8 +363,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
end
|
||||
@ -380,8 +389,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
end
|
||||
@ -447,8 +455,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
|
||||
%{
|
||||
@ -467,8 +474,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -528,8 +534,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -576,8 +581,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -612,8 +616,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: true, time_labels: true, total_rows: true, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -676,8 +679,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
time_labels: false,
|
||||
total_rows: false
|
||||
},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
@ -707,8 +709,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
time_labels: false,
|
||||
total_rows: false
|
||||
},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
@ -741,8 +742,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
time_labels: false,
|
||||
total_rows: false
|
||||
},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
@ -799,8 +799,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 100, offset: 200},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 100, offset: 200}
|
||||
})
|
||||
end
|
||||
|
||||
@ -1125,8 +1124,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
end
|
||||
@ -1147,8 +1145,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
end
|
||||
@ -1168,8 +1165,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -1230,8 +1226,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: [{:events, :desc}, {:visitors, :asc}],
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -1251,8 +1246,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: [{"event:name", :desc}],
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -1344,45 +1338,51 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
)
|
||||
end
|
||||
|
||||
# test "succeeds with event:goal filter", %{site: site} do
|
||||
# insert(:goal, %{site: site, event_name: "Signup"})
|
||||
test "succeeds with event:goal filter", %{site: site} do
|
||||
insert(:goal, %{site: site, event_name: "Signup"})
|
||||
insert(:goal, %{site: site, event_name: "Purchase", currency: "USD"})
|
||||
|
||||
# %{
|
||||
# "metrics" => ["conversion_rate"],
|
||||
# "date_range" => "all",
|
||||
# "filters" => [["is", "event:goal", ["Signup"]]]
|
||||
# }
|
||||
# |> check_success(site, %{
|
||||
# metrics: [:conversion_rate],
|
||||
# utc_time_range: @date_range_day,
|
||||
# filters: [[:is, "event:goal", [event: "Signup"]]],
|
||||
# dimensions: [],
|
||||
# order_by: nil,
|
||||
# timezone: site.timezone,
|
||||
# include: %{imports: false, time_labels: false},
|
||||
# preloaded_goals: [event: "Signup"]
|
||||
# })
|
||||
# end
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["conversion_rate"],
|
||||
"date_range" => "all",
|
||||
"filters" => [["is", "event:goal", ["Signup"]]]
|
||||
}
|
||||
|> check_success(site, %{
|
||||
metrics: [:conversion_rate],
|
||||
utc_time_range: @date_range_day,
|
||||
filters: [[:is, "event:goal", ["Signup"]]],
|
||||
dimensions: [],
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
|> check_goals(preloaded_goals: ["Signup"], revenue_currencies: %{})
|
||||
end
|
||||
|
||||
# test "succeeds with event:goal dimension", %{site: site} do
|
||||
# goal = insert(:goal, %{site: site, event_name: "Signup"})
|
||||
test "succeeds with event:goal dimension", %{site: site} do
|
||||
insert(:goal, %{site: site, event_name: "Purchase", currency: "USD"})
|
||||
insert(:goal, %{site: site, event_name: "Signup"})
|
||||
|
||||
# %{
|
||||
# "metrics" => ["conversion_rate"],
|
||||
# "date_range" => "all",
|
||||
# "dimensions" => ["event:goal"]
|
||||
# }
|
||||
# |> check_success(site, %{
|
||||
# metrics: [:conversion_rate],
|
||||
# utc_time_range: @date_range_day,
|
||||
# filters: [],
|
||||
# dimensions: ["event:goal"],
|
||||
# order_by: nil,
|
||||
# timezone: site.timezone,
|
||||
# include: %{imports: false, time_labels: false},
|
||||
# preloaded_goals: [goal]
|
||||
# })
|
||||
# end
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["conversion_rate"],
|
||||
"date_range" => "all",
|
||||
"dimensions" => ["event:goal"]
|
||||
}
|
||||
|> check_success(site, %{
|
||||
metrics: [:conversion_rate],
|
||||
utc_time_range: @date_range_day,
|
||||
filters: [],
|
||||
dimensions: ["event:goal"],
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
|> check_goals(preloaded_goals: ["Purchase", "Signup"], revenue_currencies: %{})
|
||||
end
|
||||
|
||||
test "custom properties filter with special metric", %{site: site} do
|
||||
%{
|
||||
@ -1402,8 +1402,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -1423,25 +1422,27 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
end
|
||||
|
||||
describe "views_per_visit metric" do
|
||||
# test "succeeds with normal filters", %{site: site} do
|
||||
# insert(:goal, %{site: site, event_name: "Signup"})
|
||||
test "succeeds with normal filters", %{site: site} do
|
||||
insert(:goal, %{site: site, event_name: "Signup"})
|
||||
|
||||
# %{
|
||||
# "metrics" => ["views_per_visit"],
|
||||
# "date_range" => "all",
|
||||
# "filters" => [["is", "event:goal", ["Signup"]]]
|
||||
# }
|
||||
# |> check_success(site, %{
|
||||
# metrics: [:views_per_visit],
|
||||
# utc_time_range: @date_range_day,
|
||||
# filters: [[:is, "event:goal", [event: "Signup"]]],
|
||||
# dimensions: [],
|
||||
# order_by: nil,
|
||||
# timezone: site.timezone,
|
||||
# include: %{imports: false, time_labels: false},
|
||||
# preloaded_goals: [event: "Signup"]
|
||||
# })
|
||||
# end
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["views_per_visit"],
|
||||
"date_range" => "all",
|
||||
"filters" => [["is", "event:goal", ["Signup"]]]
|
||||
}
|
||||
|> check_success(site, %{
|
||||
metrics: [:views_per_visit],
|
||||
utc_time_range: @date_range_day,
|
||||
filters: [[:is, "event:goal", ["Signup"]]],
|
||||
dimensions: [],
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
|> check_goals(preloaded_goals: ["Signup"], revenue_currencies: %{})
|
||||
end
|
||||
|
||||
test "fails validation if event:page filter specified", %{site: site} do
|
||||
%{
|
||||
@ -1470,6 +1471,209 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "revenue metrics" do
|
||||
@describetag :ee_only
|
||||
|
||||
setup %{user: user} do
|
||||
plan =
|
||||
insert(:enterprise_plan,
|
||||
features: [Plausible.Billing.Feature.RevenueGoals],
|
||||
user_id: user.id
|
||||
)
|
||||
|
||||
subscription = insert(:subscription, user: user, paddle_plan_id: plan.paddle_plan_id)
|
||||
|
||||
{:ok, subscription: subscription}
|
||||
end
|
||||
|
||||
test "not valid in public schema", %{site: site} do
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["total_revenue", "average_revenue"],
|
||||
"date_range" => "all"
|
||||
}
|
||||
|> check_error(
|
||||
site,
|
||||
"#/metrics/0: Invalid metric \"total_revenue\"\n#/metrics/1: Invalid metric \"average_revenue\""
|
||||
)
|
||||
end
|
||||
|
||||
test "can request in internal schema", %{site: site} do
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["total_revenue", "average_revenue"],
|
||||
"date_range" => "all"
|
||||
}
|
||||
|> check_success(
|
||||
site,
|
||||
%{
|
||||
metrics: [:total_revenue, :average_revenue],
|
||||
utc_time_range: @date_range_day,
|
||||
filters: [],
|
||||
dimensions: [],
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
end
|
||||
|
||||
test "no access", %{site: site, user: user, subscription: subscription} do
|
||||
Repo.delete!(subscription)
|
||||
|
||||
plan =
|
||||
insert(:enterprise_plan, features: [Plausible.Billing.Feature.StatsAPI], user_id: user.id)
|
||||
|
||||
insert(:subscription, user: user, paddle_plan_id: plan.paddle_plan_id)
|
||||
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["total_revenue", "average_revenue"],
|
||||
"date_range" => "all"
|
||||
}
|
||||
|> check_error(
|
||||
site,
|
||||
"The owner of this site does not have access to the revenue metrics feature.",
|
||||
:internal
|
||||
)
|
||||
end
|
||||
|
||||
test "with event:goal filters with same currency", %{site: site} do
|
||||
insert(:goal,
|
||||
site: site,
|
||||
event_name: "Purchase",
|
||||
currency: "USD",
|
||||
display_name: "PurchaseUSD"
|
||||
)
|
||||
|
||||
insert(:goal, site: site, event_name: "Subscription", currency: "USD")
|
||||
insert(:goal, site: site, event_name: "Signup")
|
||||
insert(:goal, site: site, event_name: "Logout")
|
||||
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["total_revenue", "average_revenue"],
|
||||
"date_range" => "all",
|
||||
"filters" => [["is", "event:goal", ["PurchaseUSD", "Signup", "Subscription"]]]
|
||||
}
|
||||
|> check_success(
|
||||
site,
|
||||
%{
|
||||
metrics: [:total_revenue, :average_revenue],
|
||||
utc_time_range: @date_range_day,
|
||||
filters: [[:is, "event:goal", ["PurchaseUSD", "Signup", "Subscription"]]],
|
||||
dimensions: [],
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
|> check_goals(
|
||||
preloaded_goals: ["PurchaseUSD", "Signup", "Subscription"],
|
||||
revenue_currencies: %{default: :USD}
|
||||
)
|
||||
end
|
||||
|
||||
test "with event:goal filters with different currencies", %{site: site} do
|
||||
insert(:goal, site: site, event_name: "Purchase", currency: "USD")
|
||||
insert(:goal, site: site, event_name: "Subscription", currency: "EUR")
|
||||
insert(:goal, site: site, event_name: "Signup")
|
||||
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["total_revenue", "average_revenue"],
|
||||
"date_range" => "all",
|
||||
"filters" => [["is", "event:goal", ["Purchase", "Signup", "Subscription"]]]
|
||||
}
|
||||
|> check_success(
|
||||
site,
|
||||
%{
|
||||
metrics: [:total_revenue, :average_revenue],
|
||||
utc_time_range: @date_range_day,
|
||||
filters: [[:is, "event:goal", ["Purchase", "Signup", "Subscription"]]],
|
||||
dimensions: [],
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
|> check_goals(
|
||||
preloaded_goals: ["Purchase", "Signup", "Subscription"],
|
||||
revenue_currencies: %{}
|
||||
)
|
||||
end
|
||||
|
||||
test "with event:goal dimension, different currencies", %{site: site} do
|
||||
insert(:goal, site: site, event_name: "Purchase", currency: "USD")
|
||||
insert(:goal, site: site, event_name: "Donation", currency: "EUR")
|
||||
insert(:goal, site: site, event_name: "Signup")
|
||||
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["total_revenue", "average_revenue"],
|
||||
"date_range" => "all",
|
||||
"dimensions" => ["event:goal"]
|
||||
}
|
||||
|> check_success(
|
||||
site,
|
||||
%{
|
||||
metrics: [:total_revenue, :average_revenue],
|
||||
utc_time_range: @date_range_day,
|
||||
filters: [],
|
||||
dimensions: ["event:goal"],
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
|> check_goals(
|
||||
preloaded_goals: ["Donation", "Purchase", "Signup"],
|
||||
revenue_currencies: %{"Donation" => :EUR, "Purchase" => :USD}
|
||||
)
|
||||
end
|
||||
|
||||
test "with event:goal dimension and filters", %{site: site} do
|
||||
insert(:goal, site: site, event_name: "Purchase", currency: "USD")
|
||||
insert(:goal, site: site, event_name: "Subscription", currency: "USD")
|
||||
insert(:goal, site: site, event_name: "Signup")
|
||||
insert(:goal, site: site, event_name: "Logout")
|
||||
|
||||
%{
|
||||
"site_id" => site.domain,
|
||||
"metrics" => ["total_revenue", "average_revenue"],
|
||||
"date_range" => "all",
|
||||
"dimensions" => ["event:goal"],
|
||||
"filters" => [["is", "event:goal", ["Purchase", "Signup"]]]
|
||||
}
|
||||
|> check_success(
|
||||
site,
|
||||
%{
|
||||
metrics: [:total_revenue, :average_revenue],
|
||||
utc_time_range: @date_range_day,
|
||||
filters: [[:is, "event:goal", ["Purchase", "Signup"]]],
|
||||
dimensions: ["event:goal"],
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
},
|
||||
:internal
|
||||
)
|
||||
|> check_goals(
|
||||
preloaded_goals: ["Purchase", "Signup"],
|
||||
revenue_currencies: %{"Purchase" => :USD}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "session metrics" do
|
||||
test "single session metric succeeds", %{site: site} do
|
||||
%{
|
||||
@ -1486,8 +1690,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -1519,8 +1722,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
|
||||
@ -1539,8 +1741,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
|
||||
order_by: nil,
|
||||
timezone: site.timezone,
|
||||
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
|
||||
pagination: %{limit: 10_000, offset: 0},
|
||||
preloaded_goals: []
|
||||
pagination: %{limit: 10_000, offset: 0}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
@ -1451,6 +1451,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
|
||||
} in top_stats
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "does not return average and total when filtering by many revenue goals with different currencies",
|
||||
%{conn: conn, site: site} do
|
||||
insert(:goal, site: site, event_name: "Payment", currency: "USD")
|
||||
@ -1488,6 +1489,93 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
|
||||
refute "Total revenue" in metrics
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "returns average and total revenue when filtering by many goals some which don't have currencies",
|
||||
%{conn: conn, site: site} do
|
||||
insert(:goal, site: site, event_name: "Payment", currency: "USD")
|
||||
insert(:goal, site: site, event_name: "Signup")
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Payment",
|
||||
revenue_reporting_amount: Decimal.new(1_000),
|
||||
revenue_reporting_currency: "USD"
|
||||
),
|
||||
build(:event,
|
||||
name: "Payment",
|
||||
revenue_reporting_amount: Decimal.new(1_000),
|
||||
revenue_reporting_currency: "USD"
|
||||
),
|
||||
build(:event, name: "Signup"),
|
||||
build(:event, name: "Signup")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{goal: "Payment|Signup"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/top-stats?period=all&filters=#{filters}")
|
||||
assert %{"top_stats" => top_stats} = json_response(conn, 200)
|
||||
|
||||
assert %{
|
||||
"name" => "Average revenue",
|
||||
"value" => %{"long" => "$1,000.00", "short" => "$1.0K"},
|
||||
"graph_metric" => "average_revenue"
|
||||
} in top_stats
|
||||
|
||||
assert %{
|
||||
"name" => "Total revenue",
|
||||
"value" => %{"long" => "$2,000.00", "short" => "$2.0K"},
|
||||
"graph_metric" => "total_revenue"
|
||||
} in top_stats
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "returns average and total revenue when no conversions",
|
||||
%{conn: conn, site: site} do
|
||||
insert(:goal, site: site, event_name: "Payment", currency: "USD")
|
||||
|
||||
filters = Jason.encode!(%{goal: "Payment|Signup"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/top-stats?period=all&filters=#{filters}")
|
||||
assert %{"top_stats" => top_stats} = json_response(conn, 200)
|
||||
|
||||
assert %{
|
||||
"name" => "Average revenue",
|
||||
"value" => %{"long" => "$0.00", "short" => "$0.0"},
|
||||
"graph_metric" => "average_revenue"
|
||||
} in top_stats
|
||||
|
||||
assert %{
|
||||
"name" => "Total revenue",
|
||||
"value" => %{"long" => "$0.00", "short" => "$0.0"},
|
||||
"graph_metric" => "total_revenue"
|
||||
} in top_stats
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "does not return average and total revenue when filtering non-currency goal",
|
||||
%{conn: conn, site: site} do
|
||||
insert(:goal, site: site, event_name: "Payment", display_name: "PaymentWithoutCurrency")
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Payment",
|
||||
revenue_reporting_amount: Decimal.new(1_000),
|
||||
revenue_reporting_currency: "USD"
|
||||
),
|
||||
build(:event,
|
||||
name: "Payment",
|
||||
revenue_reporting_amount: Decimal.new(1_000),
|
||||
revenue_reporting_currency: "USD"
|
||||
)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{goal: "PaymentWithoutCurrency"})
|
||||
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
|
||||
|
||||
test "does not return average and total when site owner is on a growth plan",
|
||||
%{conn: conn, site: site, user: user} do
|
||||
insert(:growth_subscription, user: user)
|
||||
|
Loading…
Reference in New Issue
Block a user