Skip to content

Commit e5dbd63

Browse files
committed
Make revenue metrics available on ee, implement a warnings system
1 parent 43ef172 commit e5dbd63

File tree

9 files changed

+747
-227
lines changed

9 files changed

+747
-227
lines changed

extra/lib/plausible/stats/goal/revenue.ex

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ defmodule Plausible.Stats.Goal.Revenue do
1010
end
1111

1212
@doc """
13-
Preloads revenue currencies for a query.
13+
Preloads revenue currencies for a query. Used when parsing the query.
14+
15+
Returns tuple containing revenue warning (if set, no revenue metrics should be calculated) and
16+
revenue currencies map.
1417
1518
Assumptions and business logic:
1619
1. Goals are already filtered according to query filters and dimensions
@@ -21,26 +24,33 @@ defmodule Plausible.Stats.Goal.Revenue do
2124
2225
The resulting data structure is attached to a `Query` and used below in `format_revenue_metric/3`.
2326
"""
24-
def preload_revenue_currencies(site, goals, metrics, dimensions) do
25-
if requested?(metrics) and length(goals) > 0 and available?(site) do
26-
goal_currency_map =
27-
goals
28-
|> Map.new(fn goal -> {Plausible.Goal.display_name(goal), goal.currency} end)
29-
|> Map.reject(fn {_goal, currency} -> is_nil(currency) end)
30-
31-
currencies = goal_currency_map |> Map.values() |> Enum.uniq()
32-
goal_dimension? = "event:goal" in dimensions
33-
34-
case {currencies, goal_dimension?} do
35-
{[currency], false} -> %{default: currency}
36-
{_, true} -> goal_currency_map
37-
_ -> %{}
38-
end
39-
else
40-
%{}
27+
def preload(site, goals, metrics, dimensions) do
28+
cond do
29+
not requested?(metrics) -> {nil, %{}}
30+
not available?(site) -> {:revenue_goals_unavailable, %{}}
31+
true -> preload(goals, dimensions)
32+
end
33+
end
34+
35+
defp preload(goals, dimensions) do
36+
goal_currency_map =
37+
goals
38+
|> Map.new(fn goal -> {Plausible.Goal.display_name(goal), goal.currency} end)
39+
|> Map.reject(fn {_goal, currency} -> is_nil(currency) end)
40+
41+
currencies = goal_currency_map |> Map.values() |> Enum.uniq()
42+
goal_dimension? = "event:goal" in dimensions
43+
44+
case {currencies, goal_dimension?} do
45+
{[currency], false} -> {nil, %{default: currency}}
46+
{[], _} -> {:no_revenue_goals_matching, %{}}
47+
{_, true} -> {nil, goal_currency_map}
48+
_ -> {:no_single_revenue_currency, %{}}
4149
end
4250
end
4351

52+
def format_revenue_metric(_, query, _) when not is_nil(query.revenue_warning), do: nil
53+
4454
def format_revenue_metric(value, query, dimension_values) do
4555
currency =
4656
query.revenue_currencies[:default] ||

lib/plausible/stats/filters/query_parser.ex

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ defmodule Plausible.Stats.Filters.QueryParser do
3838
{:ok, order_by} <- parse_order_by(Map.get(params, "order_by")),
3939
{:ok, include} <- parse_include(site, Map.get(params, "include", %{})),
4040
{:ok, pagination} <- parse_pagination(Map.get(params, "pagination", %{})),
41-
{preloaded_goals, revenue_currencies} <-
42-
preload_needed_goals(site, metrics, filters, dimensions),
41+
{preloaded_goals, revenue_warning, revenue_currencies} <-
42+
preload_goals_and_revenue(site, metrics, filters, dimensions),
4343
query = %{
4444
metrics: metrics,
4545
filters: filters,
@@ -50,6 +50,7 @@ defmodule Plausible.Stats.Filters.QueryParser do
5050
include: include,
5151
pagination: pagination,
5252
preloaded_goals: preloaded_goals,
53+
revenue_warning: revenue_warning,
5354
revenue_currencies: revenue_currencies
5455
},
5556
:ok <- validate_order_by(query),
@@ -422,20 +423,24 @@ defmodule Plausible.Stats.Filters.QueryParser do
422423
end
423424
end
424425

425-
def preload_needed_goals(site, metrics, filters, dimensions) do
426+
def preload_goals_and_revenue(site, metrics, filters, dimensions) do
426427
goal_filters? =
427428
Enum.any?(filters, fn [_, filter_key | _rest] -> filter_key == "event:goal" end)
428429

429-
if goal_filters? or Enum.member?(dimensions, "event:goal") do
430-
goals = Plausible.Goals.Filters.preload_needed_goals(site, filters)
430+
goals =
431+
if goal_filters? or Enum.member?(dimensions, "event:goal") do
432+
Plausible.Goals.Filters.preload_needed_goals(site, filters)
433+
else
434+
[]
435+
end
431436

432-
{
433-
goals,
434-
preload_revenue_currencies(site, goals, metrics, dimensions)
435-
}
436-
else
437-
{[], %{}}
438-
end
437+
{revenue_warning, revenue_currencies} = preload_revenue(site, goals, metrics, dimensions)
438+
439+
{
440+
goals,
441+
revenue_warning,
442+
revenue_currencies
443+
}
439444
end
440445

441446
@only_toplevel ["event:goal", "event:hostname"]
@@ -485,8 +490,9 @@ defmodule Plausible.Stats.Filters.QueryParser do
485490
on_ee do
486491
alias Plausible.Stats.Goal.Revenue
487492

488-
defdelegate preload_revenue_currencies(site, preloaded_goals, metrics, dimensions),
489-
to: Plausible.Stats.Goal.Revenue
493+
def preload_revenue(site, preloaded_goals, metrics, dimensions) do
494+
Revenue.preload(site, preloaded_goals, metrics, dimensions)
495+
end
490496

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

501507
defp validate_revenue_metrics_access(_site, _query), do: :ok
502508
end

lib/plausible/stats/json_schema.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule Plausible.Stats.JSONSchema do
55
Note that `internal` queries expose some metrics, filter types and other features not
66
available on the public API.
77
"""
8+
use Plausible
89
alias Plausible.Stats.JSONSchema.Utils
910

1011
@external_resource "priv/json-schemas/query-api-schema.json"
@@ -13,8 +14,14 @@ defmodule Plausible.Stats.JSONSchema do
1314
|> File.read!()
1415
|> Jason.decode!()
1516
@raw_public_schema Utils.traverse(@raw_internal_schema, fn
16-
%{"$comment" => "only :internal"} -> :remove
17-
value -> value
17+
%{"$comment" => "only :internal"} ->
18+
:remove
19+
20+
%{"$comment" => "only :ee"} = value ->
21+
if(ee?(), do: Map.delete(value, "$comment"), else: :remove)
22+
23+
value ->
24+
value
1825
end)
1926
@internal_query_schema ExJsonSchema.Schema.resolve(@raw_internal_schema)
2027
@public_query_schema ExJsonSchema.Schema.resolve(@raw_public_schema)

lib/plausible/stats/legacy/legacy_query_builder.ex

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ defmodule Plausible.Stats.Legacy.QueryBuilder do
1919
|> put_dimensions(params)
2020
|> put_interval(params)
2121
|> put_parsed_filters(params)
22-
|> put_preloaded_goals(site)
22+
|> preload_goals_and_revenue(site)
2323
|> put_order_by(params)
2424
|> put_include_comparisons(site, params)
2525
|> Query.put_imported_opts(site, params)
@@ -31,16 +31,20 @@ defmodule Plausible.Stats.Legacy.QueryBuilder do
3131
query
3232
end
3333

34-
defp put_preloaded_goals(query, site) do
35-
{preloaded_goals, revenue_currencies} =
36-
Plausible.Stats.Filters.QueryParser.preload_needed_goals(
34+
defp preload_goals_and_revenue(query, site) do
35+
{preloaded_goals, revenue_warning, revenue_currencies} =
36+
Plausible.Stats.Filters.QueryParser.preload_goals_and_revenue(
3737
site,
3838
query.metrics,
3939
query.filters,
4040
query.dimensions
4141
)
4242

43-
struct!(query, preloaded_goals: preloaded_goals, revenue_currencies: revenue_currencies)
43+
struct!(query,
44+
preloaded_goals: preloaded_goals,
45+
revenue_warning: revenue_warning,
46+
revenue_currencies: revenue_currencies
47+
)
4448
end
4549

4650
defp put_period(%Query{now: now} = query, _site, %{"period" => period})

lib/plausible/stats/query.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ defmodule Plausible.Stats.Query do
1515
order_by: nil,
1616
timezone: nil,
1717
legacy_breakdown: false,
18-
remove_unavailable_revenue_metrics: false,
1918
preloaded_goals: [],
20-
revenue_currencies: %{},
2119
include: Plausible.Stats.Filters.QueryParser.default_include(),
2220
debug_metadata: %{},
23-
pagination: nil
21+
pagination: nil,
22+
# Revenue metric specific metadata
23+
revenue_currencies: %{},
24+
revenue_warning: nil,
25+
remove_unavailable_revenue_metrics: false
2426

2527
require OpenTelemetry.Tracer, as: Tracer
2628
alias Plausible.Stats.{DateTimeRange, Filters, Imported, Legacy}

lib/plausible/stats/query_result.ex

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,21 @@ defmodule Plausible.Stats.QueryResult do
4040
)
4141
end
4242

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

46-
@imports_unsupported_interval_warning "Imported stats are not included because the time dimension (i.e. the interval) is too short."
51+
@revenue_metrics_warnings %{
52+
revenue_goals_unavailable:
53+
"The owner of this site does not have access to the revenue metrics feature.",
54+
no_single_revenue_currency:
55+
"Revenue metrics are null as there are multiple currencies for the selected event:goals.",
56+
no_revenue_goals_matching: "Revenue metrics are null as there are no matching revenue goals."
57+
}
4758

4859
defp meta(query, meta_extra) do
4960
%{
@@ -52,18 +63,14 @@ defmodule Plausible.Stats.QueryResult do
5263
if(query.include.imports and query.skip_imported_reason,
5364
do: to_string(query.skip_imported_reason)
5465
),
55-
imports_warning:
56-
case query.skip_imported_reason do
57-
:unsupported_query -> @imports_unsupported_query_warning
58-
:unsupported_interval -> @imports_unsupported_interval_warning
59-
_ -> nil
60-
end,
66+
imports_warning: @imports_warnings[query.skip_imported_reason],
67+
metric_warnings: metric_warnings(query),
6168
time_labels:
6269
if(query.include.time_labels, do: Plausible.Stats.Time.time_labels(query), else: nil),
6370
total_rows: if(query.include.total_rows, do: meta_extra.total_rows, else: nil)
6471
}
6572
|> Enum.reject(fn {_, value} -> is_nil(value) end)
66-
|> Enum.into(%{})
73+
|> Map.new()
6774
end
6875

6976
defp include(query) do
@@ -80,6 +87,23 @@ defmodule Plausible.Stats.QueryResult do
8087
end
8188
end
8289

90+
defp metric_warnings(query) do
91+
if query.revenue_warning do
92+
query.metrics
93+
|> Enum.filter(&(&1 in Plausible.Stats.Goal.Revenue.revenue_metrics()))
94+
|> Enum.map(
95+
&{&1,
96+
%{
97+
code: query.revenue_warning,
98+
warning: @revenue_metrics_warnings[query.revenue_warning]
99+
}}
100+
)
101+
|> Map.new()
102+
else
103+
nil
104+
end
105+
end
106+
83107
defp to_iso8601(datetime, timezone) do
84108
datetime
85109
|> DateTime.shift_zone!(timezone)

priv/json-schemas/query-api-schema.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,13 @@
264264
},
265265
{
266266
"const": "total_revenue",
267-
"$comment": "only :internal"
267+
"markdownDescription": "Total revenue",
268+
"$comment": "only :ee"
268269
},
269270
{
270271
"const": "average_revenue",
271-
"$comment": "only :internal"
272+
"markdownDescription": "Average revenue",
273+
"$comment": "only :ee"
272274
},
273275
{
274276
"const": "scroll_depth",

0 commit comments

Comments
 (0)