Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions extra/lib/plausible/stats/goal/revenue.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ defmodule Plausible.Stats.Goal.Revenue do
end

@doc """
Preloads revenue currencies for a query.
Preloads revenue currencies for a query. Used when parsing the query.

Returns tuple containing revenue warning (if set, no revenue metrics should be calculated) and
revenue currencies map.

Assumptions and business logic:
1. Goals are already filtered according to query filters and dimensions
Expand All @@ -21,26 +24,33 @@ defmodule Plausible.Stats.Goal.Revenue do

The resulting data structure is attached to a `Query` and used below in `format_revenue_metric/3`.
"""
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
else
%{}
def preload(site, goals, metrics, dimensions) do
cond do
not requested?(metrics) -> {nil, %{}}
not available?(site) -> {:revenue_goals_unavailable, %{}}
true -> preload(goals, dimensions)
end
end

defp preload(goals, dimensions) 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} -> {nil, %{default: currency}}
{[], _} -> {:no_revenue_goals_matching, %{}}
{_, true} -> {nil, goal_currency_map}
_ -> {:no_single_revenue_currency, %{}}
end
end

def format_revenue_metric(_, query, _) when not is_nil(query.revenue_warning), do: nil

def format_revenue_metric(value, query, dimension_values) do
currency =
query.revenue_currencies[:default] ||
Expand All @@ -52,7 +62,8 @@ defmodule Plausible.Stats.Goal.Revenue do
%{
short: Money.to_string!(money, format: :short, fractional_digits: 1),
long: Money.to_string!(money),
value: Decimal.to_float(money.amount)
value: Decimal.to_float(money.amount),
currency: currency
}
else
value
Expand Down
36 changes: 21 additions & 15 deletions lib/plausible/stats/filters/query_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +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, revenue_currencies} <-
preload_needed_goals(site, metrics, filters, dimensions),
{preloaded_goals, revenue_warning, revenue_currencies} <-
preload_goals_and_revenue(site, metrics, filters, dimensions),
query = %{
metrics: metrics,
filters: filters,
Expand All @@ -50,6 +50,7 @@ defmodule Plausible.Stats.Filters.QueryParser do
include: include,
pagination: pagination,
preloaded_goals: preloaded_goals,
revenue_warning: revenue_warning,
revenue_currencies: revenue_currencies
},
:ok <- validate_order_by(query),
Expand Down Expand Up @@ -422,20 +423,24 @@ defmodule Plausible.Stats.Filters.QueryParser do
end
end

def preload_needed_goals(site, metrics, filters, dimensions) do
def preload_goals_and_revenue(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
goals = Plausible.Goals.Filters.preload_needed_goals(site, filters)
goals =
if goal_filters? or Enum.member?(dimensions, "event:goal") do
Plausible.Goals.Filters.preload_needed_goals(site, filters)
else
[]
end

{
goals,
preload_revenue_currencies(site, goals, metrics, dimensions)
}
else
{[], %{}}
end
{revenue_warning, revenue_currencies} = preload_revenue(site, goals, metrics, dimensions)

{
goals,
revenue_warning,
revenue_currencies
}
end

@only_toplevel ["event:goal", "event:hostname"]
Expand Down Expand Up @@ -485,8 +490,9 @@ defmodule Plausible.Stats.Filters.QueryParser do
on_ee do
alias Plausible.Stats.Goal.Revenue

defdelegate preload_revenue_currencies(site, preloaded_goals, metrics, dimensions),
to: Plausible.Stats.Goal.Revenue
def preload_revenue(site, preloaded_goals, metrics, dimensions) do
Revenue.preload(site, preloaded_goals, metrics, dimensions)
end

defp validate_revenue_metrics_access(site, query) do
if Revenue.requested?(query.metrics) and not Revenue.available?(site) do
Expand All @@ -496,7 +502,7 @@ defmodule Plausible.Stats.Filters.QueryParser do
end
end
else
defp preload_revenue_currencies(_site, _preloaded_goals, _metrics, _dimensions), do: %{}
defp preload_revenue(_site, _preloaded_goals, _metrics, _dimensions), do: {nil, %{}}

defp validate_revenue_metrics_access(_site, _query), do: :ok
end
Expand Down
11 changes: 9 additions & 2 deletions lib/plausible/stats/json_schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Plausible.Stats.JSONSchema do
Note that `internal` queries expose some metrics, filter types and other features not
available on the public API.
"""
use Plausible
alias Plausible.Stats.JSONSchema.Utils

@external_resource "priv/json-schemas/query-api-schema.json"
Expand All @@ -13,8 +14,14 @@ defmodule Plausible.Stats.JSONSchema do
|> File.read!()
|> Jason.decode!()
@raw_public_schema Utils.traverse(@raw_internal_schema, fn
%{"$comment" => "only :internal"} -> :remove
value -> value
%{"$comment" => "only :internal"} ->
:remove

%{"$comment" => "only :ee"} = value ->
if(ee?(), do: Map.delete(value, "$comment"), else: :remove)

value ->
value
end)
@internal_query_schema ExJsonSchema.Schema.resolve(@raw_internal_schema)
@public_query_schema ExJsonSchema.Schema.resolve(@raw_public_schema)
Expand Down
14 changes: 9 additions & 5 deletions lib/plausible/stats/legacy/legacy_query_builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defmodule Plausible.Stats.Legacy.QueryBuilder do
|> put_dimensions(params)
|> put_interval(params)
|> put_parsed_filters(params)
|> put_preloaded_goals(site)
|> preload_goals_and_revenue(site)
|> put_order_by(params)
|> put_include_comparisons(site, params)
|> Query.put_imported_opts(site, params)
Expand All @@ -31,16 +31,20 @@ defmodule Plausible.Stats.Legacy.QueryBuilder do
query
end

defp put_preloaded_goals(query, site) do
{preloaded_goals, revenue_currencies} =
Plausible.Stats.Filters.QueryParser.preload_needed_goals(
defp preload_goals_and_revenue(query, site) do
{preloaded_goals, revenue_warning, revenue_currencies} =
Plausible.Stats.Filters.QueryParser.preload_goals_and_revenue(
site,
query.metrics,
query.filters,
query.dimensions
)

struct!(query, preloaded_goals: preloaded_goals, revenue_currencies: revenue_currencies)
struct!(query,
preloaded_goals: preloaded_goals,
revenue_warning: revenue_warning,
revenue_currencies: revenue_currencies
)
end

defp put_period(%Query{now: now} = query, _site, %{"period" => period})
Expand Down
8 changes: 5 additions & 3 deletions lib/plausible/stats/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ defmodule Plausible.Stats.Query do
order_by: nil,
timezone: nil,
legacy_breakdown: false,
remove_unavailable_revenue_metrics: false,
preloaded_goals: [],
revenue_currencies: %{},
include: Plausible.Stats.Filters.QueryParser.default_include(),
debug_metadata: %{},
pagination: nil
pagination: nil,
# Revenue metric specific metadata
revenue_currencies: %{},
revenue_warning: nil,
remove_unavailable_revenue_metrics: false

require OpenTelemetry.Tracer, as: Tracer
alias Plausible.Stats.{DateTimeRange, Filters, Imported, Legacy}
Expand Down
52 changes: 41 additions & 11 deletions lib/plausible/stats/query_result.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Plausible.Stats.QueryResult do
produced by Jason.encode(query_result) is ordered.
"""

use Plausible
alias Plausible.Stats.DateTimeRange

defstruct results: [],
Expand Down Expand Up @@ -40,10 +41,13 @@ defmodule Plausible.Stats.QueryResult do
)
end

@imports_unsupported_query_warning "Imported stats are not included in the results because query parameters are not supported. " <>
"For more information, see: https://plausible.io/docs/stats-api#filtering-imported-stats"

@imports_unsupported_interval_warning "Imported stats are not included because the time dimension (i.e. the interval) is too short."
@imports_warnings %{
unsupported_query:
"Imported stats are not included in the results because query parameters are not supported. " <>
"For more information, see: https://plausible.io/docs/stats-api#filtering-imported-stats",
unsupported_interval:
"Imported stats are not included because the time dimension (i.e. the interval) is too short."
}

defp meta(query, meta_extra) do
%{
Expand All @@ -52,18 +56,14 @@ defmodule Plausible.Stats.QueryResult do
if(query.include.imports and query.skip_imported_reason,
do: to_string(query.skip_imported_reason)
),
imports_warning:
case query.skip_imported_reason do
:unsupported_query -> @imports_unsupported_query_warning
:unsupported_interval -> @imports_unsupported_interval_warning
_ -> nil
end,
imports_warning: @imports_warnings[query.skip_imported_reason],
metric_warnings: metric_warnings(query),
time_labels:
if(query.include.time_labels, do: Plausible.Stats.Time.time_labels(query), else: nil),
total_rows: if(query.include.total_rows, do: meta_extra.total_rows, else: nil)
}
|> Enum.reject(fn {_, value} -> is_nil(value) end)
|> Enum.into(%{})
|> Map.new()
end

defp include(query) do
Expand All @@ -80,6 +80,36 @@ defmodule Plausible.Stats.QueryResult do
end
end

on_ee do
@revenue_metrics_warnings %{
revenue_goals_unavailable:
"The owner of this site does not have access to the revenue metrics feature.",
no_single_revenue_currency:
"Revenue metrics are null as there are multiple currencies for the selected event:goals.",
no_revenue_goals_matching:
"Revenue metrics are null as there are no matching revenue goals."
}

defp metric_warnings(query) do
if query.revenue_warning do
query.metrics
|> Enum.filter(&(&1 in Plausible.Stats.Goal.Revenue.revenue_metrics()))
|> Enum.map(
&{&1,
%{
code: query.revenue_warning,
warning: @revenue_metrics_warnings[query.revenue_warning]
}}
)
|> Map.new()
else
nil
end
end
else
defp metric_warnings(_query), do: nil
end

defp to_iso8601(datetime, timezone) do
datetime
|> DateTime.shift_zone!(timezone)
Expand Down
6 changes: 4 additions & 2 deletions priv/json-schemas/query-api-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,13 @@
},
{
"const": "total_revenue",
"$comment": "only :internal"
"markdownDescription": "Total revenue",
"$comment": "only :ee"
},
{
"const": "average_revenue",
"$comment": "only :internal"
"markdownDescription": "Average revenue",
"$comment": "only :ee"
},
{
"const": "scroll_depth",
Expand Down
Loading
Loading