Show revenue data in all breakdowns (#5767)

* Include revenue data for all detailed API responses except entry/exit pages

* Expose revenue data in all breakdown modals except entry/exit pages

* Add revenue metrics to breakdown response only on EE

* Change query builder to enable querying event metrics \w session dimension

* Add revenue metrics to entry and exit pages breakdowns

* Expose revenue data in entry and exit pages breakdowns

* Use `argMax` for `exit_page` and `exit_page_hostname` dimensions (h/t @ukutaht)

* Don't handle event-only dimensions with session-only metrics for now

* Add tests for all breakdowns

* Add clarifying comments in code

* Mark revenue tests as EE-only
This commit is contained in:
Adrian Gruntkowski 2025-11-18 12:24:54 +01:00 committed by GitHub
parent 35f1cea344
commit a2ba1256d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2081 additions and 50 deletions

View File

@ -52,7 +52,7 @@ function BrowserVersionsModal() {
<Modal>
<BreakdownModal
reportInfo={reportInfo}
metrics={chooseMetrics(query)}
metrics={chooseMetrics(query, site)}
getFilterInfo={getFilterInfo}
addSearchFilter={addSearchFilter}
renderIcon={renderIcon}

View File

@ -52,7 +52,7 @@ function BrowsersModal() {
<Modal>
<BreakdownModal
reportInfo={reportInfo}
metrics={chooseMetrics(query)}
metrics={chooseMetrics(query, site)}
getFilterInfo={getFilterInfo}
addSearchFilter={addSearchFilter}
renderIcon={renderIcon}

View File

@ -2,9 +2,13 @@ import {
hasConversionGoalFilter,
isRealTimeDashboard
} from '../../../util/filters'
import { revenueAvailable } from '../../../query'
import * as metrics from '../../reports/metrics'
export default function chooseMetrics(query) {
export default function chooseMetrics(query, site) {
/*global BUILD_EXTRA*/
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
if (hasConversionGoalFilter(query)) {
return [
metrics.createTotalVisitors(),
@ -12,8 +16,10 @@ export default function chooseMetrics(query) {
renderLabel: (_query) => 'Conversions',
width: 'w-28'
}),
metrics.createConversionRate()
]
metrics.createConversionRate(),
showRevenueMetrics && metrics.createTotalRevenue(),
showRevenueMetrics && metrics.createAverageRevenue()
].filter((metric) => !!metric)
}
if (isRealTimeDashboard(query)) {

View File

@ -49,7 +49,7 @@ function OperatingSystemVersionsModal() {
<Modal>
<BreakdownModal
reportInfo={reportInfo}
metrics={chooseMetrics(query)}
metrics={chooseMetrics(query, site)}
getFilterInfo={getFilterInfo}
addSearchFilter={addSearchFilter}
renderIcon={renderIcon}

View File

@ -49,7 +49,7 @@ function OperatingSystemsModal() {
<Modal>
<BreakdownModal
reportInfo={reportInfo}
metrics={chooseMetrics(query)}
metrics={chooseMetrics(query, site)}
getFilterInfo={getFilterInfo}
addSearchFilter={addSearchFilter}
renderIcon={renderIcon}

View File

@ -39,7 +39,7 @@ function ScreenSizesModal() {
<Modal>
<BreakdownModal
reportInfo={reportInfo}
metrics={chooseMetrics(query)}
metrics={chooseMetrics(query, site)}
getFilterInfo={getFilterInfo}
searchEnabled={false}
renderIcon={renderIcon}

View File

@ -4,7 +4,7 @@ import {
hasConversionGoalFilter,
isRealTimeDashboard
} from '../../util/filters'
import { addFilter } from '../../query'
import { addFilter, revenueAvailable } from '../../query'
import BreakdownModal from './breakdown-modal'
import * as metrics from '../reports/metrics'
import * as url from '../../util/url'
@ -16,6 +16,9 @@ function EntryPagesModal() {
const { query } = useQueryContext()
const site = useSiteContext()
/*global BUILD_EXTRA*/
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
const reportInfo = {
title: 'Entry Pages',
dimension: 'entry_page',
@ -54,8 +57,10 @@ function EntryPagesModal() {
renderLabel: (_query) => 'Conversions',
width: 'w-28'
}),
metrics.createConversionRate()
]
metrics.createConversionRate(),
showRevenueMetrics && metrics.createTotalRevenue(),
showRevenueMetrics && metrics.createAverageRevenue()
].filter((metric) => !!metric)
}
if (isRealTimeDashboard(query)) {

View File

@ -1,7 +1,7 @@
import React, { useCallback } from 'react'
import Modal from './modal'
import { hasConversionGoalFilter } from '../../util/filters'
import { addFilter } from '../../query'
import { addFilter, revenueAvailable } from '../../query'
import BreakdownModal from './breakdown-modal'
import * as metrics from '../reports/metrics'
import * as url from '../../util/url'
@ -13,6 +13,9 @@ function ExitPagesModal() {
const { query } = useQueryContext()
const site = useSiteContext()
/*global BUILD_EXTRA*/
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
const reportInfo = {
title: 'Exit Pages',
dimension: 'exit_page',
@ -51,8 +54,10 @@ function ExitPagesModal() {
renderLabel: (_query) => 'Conversions',
width: 'w-28'
}),
metrics.createConversionRate()
]
metrics.createConversionRate(),
showRevenueMetrics && metrics.createTotalRevenue(),
showRevenueMetrics && metrics.createAverageRevenue()
].filter((metric) => !!metric)
}
if (query.period === 'realtime') {

View File

@ -7,7 +7,7 @@ import * as metrics from '../reports/metrics'
import * as url from '../../util/url'
import { useQueryContext } from '../../query-context'
import { useSiteContext } from '../../site-context'
import { addFilter } from '../../query'
import { addFilter, revenueAvailable } from '../../query'
import { SortDirection } from '../../hooks/use-order-by'
const VIEWS = {
@ -38,6 +38,9 @@ function LocationsModal({ currentView }) {
const { query } = useQueryContext()
const site = useSiteContext()
/*global BUILD_EXTRA*/
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
let reportInfo = VIEWS[currentView]
reportInfo = {
...reportInfo,
@ -75,8 +78,10 @@ function LocationsModal({ currentView }) {
renderLabel: (_query) => 'Conversions',
width: 'w-28'
}),
metrics.createConversionRate()
]
metrics.createConversionRate(),
showRevenueMetrics && metrics.createTotalRevenue(),
showRevenueMetrics && metrics.createAverageRevenue()
].filter((metric) => !!metric)
}
if (query.period === 'realtime') {

View File

@ -4,7 +4,7 @@ import {
hasConversionGoalFilter,
isRealTimeDashboard
} from '../../util/filters'
import { addFilter } from '../../query'
import { addFilter, revenueAvailable } from '../../query'
import BreakdownModal from './breakdown-modal'
import * as metrics from '../reports/metrics'
import * as url from '../../util/url'
@ -16,6 +16,9 @@ function PagesModal() {
const { query } = useQueryContext()
const site = useSiteContext()
/*global BUILD_EXTRA*/
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
const reportInfo = {
title: 'Top Pages',
dimension: 'page',
@ -54,8 +57,10 @@ function PagesModal() {
renderLabel: (_query) => 'Conversions',
width: 'w-28'
}),
metrics.createConversionRate()
]
metrics.createConversionRate(),
showRevenueMetrics && metrics.createTotalRevenue(),
showRevenueMetrics && metrics.createAverageRevenue()
].filter((metric) => !!metric)
}
if (isRealTimeDashboard(query)) {

View File

@ -9,7 +9,7 @@ import {
import BreakdownModal from './breakdown-modal'
import * as metrics from '../reports/metrics'
import * as url from '../../util/url'
import { addFilter } from '../../query'
import { addFilter, revenueAvailable } from '../../query'
import { useQueryContext } from '../../query-context'
import { useSiteContext } from '../../site-context'
import { SortDirection } from '../../hooks/use-order-by'
@ -20,6 +20,9 @@ function ReferrerDrilldownModal() {
const { query } = useQueryContext()
const site = useSiteContext()
/*global BUILD_EXTRA*/
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
const reportInfo = {
title: 'Referrer Drilldown',
dimension: 'referrer',
@ -61,8 +64,10 @@ function ReferrerDrilldownModal() {
renderLabel: (_query) => 'Conversions',
width: 'w-28'
}),
metrics.createConversionRate()
]
metrics.createConversionRate(),
showRevenueMetrics && metrics.createTotalRevenue(),
showRevenueMetrics && metrics.createAverageRevenue()
].filter((metric) => !!metric)
}
if (isRealTimeDashboard(query)) {

View File

@ -7,7 +7,7 @@ import {
import BreakdownModal from './breakdown-modal'
import * as metrics from '../reports/metrics'
import * as url from '../../util/url'
import { addFilter } from '../../query'
import { addFilter, revenueAvailable } from '../../query'
import { useQueryContext } from '../../query-context'
import { useSiteContext } from '../../site-context'
import { SortDirection } from '../../hooks/use-order-by'
@ -91,6 +91,9 @@ function SourcesModal({ currentView }) {
const { query } = useQueryContext()
const site = useSiteContext()
/*global BUILD_EXTRA*/
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
let reportInfo = VIEWS[currentView].info
reportInfo = {
...reportInfo,
@ -127,8 +130,10 @@ function SourcesModal({ currentView }) {
renderLabel: (_query) => 'Conversions',
width: 'w-28'
}),
metrics.createConversionRate()
]
metrics.createConversionRate(),
showRevenueMetrics && metrics.createTotalRevenue(),
showRevenueMetrics && metrics.createAverageRevenue()
].filter((metric) => !!metric)
}
if (isRealTimeDashboard(query)) {

View File

@ -190,6 +190,49 @@ defmodule Plausible.Stats.SQL.Expression do
def select_dimension(q, key, "visit:city_name", _table, _query),
do: select_merge_as(q, [t], %{key => t.city_name})
def select_dimension_internal(q, "visit:entry_page") do
select_merge_as(q, [t], %{
entry_page: fragment("any(?)", field(t, :entry_page))
})
end
def select_dimension_internal(q, "visit:entry_page_hostname") do
select_merge_as(q, [t], %{
entry_page_hostname: fragment("any(?)", field(t, :entry_page_hostname))
})
end
def select_dimension_internal(q, "visit:exit_page") do
# As exit page changes with every pageview event over the lifetime
# of a session, only the most recent value must be considered.
select_merge_as(q, [t], %{
exit_page: fragment("argMax(?, ?)", field(t, :exit_page), field(t, :events))
})
end
def select_dimension_internal(q, "visit:exit_page_hostname") do
select_merge_as(q, [t], %{
exit_page_hostname:
fragment("argMax(?, ?)", field(t, :exit_page_hostname), field(t, :events))
})
end
def select_dimension_internal(q, _dimension), do: q
def select_dimension_from_join(q, key, "visit:entry_page"),
do: select_merge_as(q, [..., t], %{key => t.entry_page})
def select_dimension_from_join(q, key, "visit:entry_page_hostname"),
do: select_merge_as(q, [..., t], %{key => t.entry_page_hostaname})
def select_dimension_from_join(q, key, "visit:exit_page"),
do: select_merge_as(q, [..., t], %{key => t.exit_page})
def select_dimension_from_join(q, key, "visit:exit_page_hostname"),
do: select_merge_as(q, [..., t], %{key => t.exit_page_hostname})
def select_dimension_from_join(q, _key, _dimension), do: q
def event_metric(:pageviews, _query) do
wrap_alias([e], %{
pageviews: scale_sample(fragment("countIf(? = 'pageview')", e.name))

View File

@ -72,6 +72,8 @@ defmodule Plausible.Stats.SQL.QueryBuilder do
defp join_sessions_if_needed(q, query) do
if TableDecider.events_join_sessions?(query) do
%{session: dimensions} = TableDecider.partition_dimensions(query)
sessions_q =
from(
s in "sessions_v2",
@ -81,6 +83,14 @@ defmodule Plausible.Stats.SQL.QueryBuilder do
group_by: s.session_id
)
# The session-only dimension columns are explicitly selected in joined
# sessions table. This enables combining session-only dimensions (entry
# and exit pages) with event-only metrics, like revenue.
sessions_q =
Enum.reduce(dimensions, sessions_q, fn dimension, acc ->
Plausible.Stats.SQL.Expression.select_dimension_internal(acc, dimension)
end)
on_ee do
sessions_q = Plausible.Stats.Sampling.add_query_hint(sessions_q, query)
end
@ -131,8 +141,31 @@ defmodule Plausible.Stats.SQL.QueryBuilder do
|> Enum.reduce(%{}, &Map.merge/2)
end
def build_group_by(q, table, query) do
Enum.reduce(query.dimensions, q, &dimension_group_by(&2, table, query, &1))
def build_group_by(q, :events, query) do
# Session-only dimensions are extracted from joined sessions table
%{session: session_only_dimensions} = TableDecider.partition_dimensions(query)
event_dimensions = query.dimensions -- session_only_dimensions
q =
Enum.reduce(event_dimensions, q, &dimension_group_by(&2, :events, query, &1))
Enum.reduce(
session_only_dimensions,
q,
&dimension_group_by_join(&2, query, &1)
)
end
def build_group_by(q, :sessions, query) do
Enum.reduce(query.dimensions, q, &dimension_group_by(&2, :sessions, query, &1))
end
defp dimension_group_by_join(q, query, dimension) do
key = shortname(query, dimension)
q
|> Expression.select_dimension_from_join(key, dimension)
|> group_by([], selected_as(^key))
end
defp dimension_group_by(q, :events, query, "event:goal" = dimension) do

View File

@ -54,6 +54,10 @@ defmodule Plausible.Stats.TableDecider do
end
end
def partition_dimensions(query) do
partition(query.dimensions, query, &dimension_partitioner/2)
end
@type table_type() :: :events | :sessions
@type metric() :: String.t()

View File

@ -477,7 +477,11 @@ defmodule PlausibleWeb.Api.StatsController do
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics)
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics,
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -511,7 +515,11 @@ defmodule PlausibleWeb.Api.StatsController do
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics)
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics,
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -595,7 +603,12 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:utm_medium")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics =
breakdown_metrics(query,
extra_metrics: [:bounce_rate, :visit_duration],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -625,7 +638,12 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:utm_campaign")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics =
breakdown_metrics(query,
extra_metrics: [:bounce_rate, :visit_duration],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -655,7 +673,12 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:utm_content")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics =
breakdown_metrics(query,
extra_metrics: [:bounce_rate, :visit_duration],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -685,7 +708,12 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:utm_term")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics =
breakdown_metrics(query,
extra_metrics: [:bounce_rate, :visit_duration],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -715,7 +743,12 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:utm_source")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics =
breakdown_metrics(query,
extra_metrics: [:bounce_rate, :visit_duration],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -745,7 +778,12 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:referrer")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics =
breakdown_metrics(query,
extra_metrics: [:bounce_rate, :visit_duration],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -837,7 +875,11 @@ defmodule PlausibleWeb.Api.StatsController do
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics)
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics,
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -865,7 +907,12 @@ defmodule PlausibleWeb.Api.StatsController do
[]
end
metrics = breakdown_metrics(query, extra_metrics)
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics,
include_revenue?: !!params["detailed"]
)
pagination = parse_pagination(params)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -897,7 +944,12 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:entry_page")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:visits, :visit_duration, :bounce_rate])
metrics =
breakdown_metrics(query,
extra_metrics: [:visits, :visit_duration, :bounce_rate],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -943,7 +995,11 @@ defmodule PlausibleWeb.Api.StatsController do
[:visits, :exit_rate]
end
metrics = breakdown_metrics(query, extra_metrics)
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics,
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, {limit, page})
@ -980,7 +1036,12 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:country")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:percentage])
metrics =
breakdown_metrics(query,
extra_metrics: [:percentage],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -1038,7 +1099,7 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:region")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
metrics = breakdown_metrics(query, include_revenue?: !!params["detailed"])
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -1079,7 +1140,7 @@ defmodule PlausibleWeb.Api.StatsController do
params = Map.put(params, "property", "visit:city")
query = Query.from(site, params, debug_metadata(conn))
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
metrics = breakdown_metrics(query, include_revenue?: !!params["detailed"])
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -1129,7 +1190,11 @@ defmodule PlausibleWeb.Api.StatsController do
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics ++ [:percentage],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -1163,7 +1228,11 @@ defmodule PlausibleWeb.Api.StatsController do
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics ++ [:percentage],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -1206,7 +1275,11 @@ defmodule PlausibleWeb.Api.StatsController do
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics ++ [:percentage],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -1240,7 +1313,11 @@ defmodule PlausibleWeb.Api.StatsController do
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics ++ [:percentage],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -1283,7 +1360,11 @@ defmodule PlausibleWeb.Api.StatsController do
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
metrics =
breakdown_metrics(query,
extra_metrics: extra_metrics ++ [:percentage],
include_revenue?: !!params["detailed"]
)
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
@ -1614,9 +1695,18 @@ defmodule PlausibleWeb.Api.StatsController do
end
end
defp breakdown_metrics(query, extra_metrics \\ []) do
defp breakdown_metrics(query, opts) do
extra_metrics = Keyword.get(opts, :extra_metrics, [])
include_revenue? = Keyword.get(opts, :include_revenue?, false)
if toplevel_goal_filter?(query) do
[:visitors, :conversion_rate, :total_visitors]
metrics = [:visitors, :conversion_rate, :total_visitors]
if ee?() and include_revenue? do
metrics ++ [:average_revenue, :total_revenue]
else
metrics
end
else
[:visitors] ++ extra_metrics
end

View File

@ -271,6 +271,114 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
"comparison_date_range_label" => "30 Dec 2020 - 5 Jan 2021"
}
end
@tag :ee_only
test "return revenue metrics for browsers breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, browser: "Firefox"),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 2, browser: "Firefox"),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 3, browser: "Firefox"),
build(:pageview, user_id: 4, browser: "Safari"),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 5, browser: "Safari"),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/browsers#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"conversion_rate" => 100.0,
"name" => "(not set)",
"total_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"total_visitors" => 2,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"name" => "Firefox",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"name" => "Safari",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2,
"visitors" => 1
}
]
end
end
describe "GET /api/stats/:domain/browser-versions" do

View File

@ -66,5 +66,124 @@ defmodule PlausibleWeb.Api.StatsController.CitiesTest do
%{"code" => 591_632, "country_flag" => "🇪🇪", "name" => "Kärdla", "visitors" => 2}
]
end
@tag :ee_only
test "return revenue metrics for cities breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
country_code: "EE",
subdivision1_code: "EE-37",
city_geoname_id: 588_409
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
country_code: "EE",
subdivision1_code: "EE-37",
city_geoname_id: 588_409
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
country_code: "EE",
subdivision1_code: "EE-37",
city_geoname_id: 588_409
),
build(:pageview,
user_id: 4,
country_code: "EE",
subdivision1_code: "EE-39",
city_geoname_id: 591_632
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
country_code: "EE",
subdivision1_code: "EE-39",
city_geoname_id: 591_632
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/cities#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 33.33,
"name" => "Tallinn",
"code" => 588_409,
"country_flag" => "🇪🇪",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 6,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 25.0,
"name" => "Kärdla",
"code" => 591_632,
"country_flag" => "🇪🇪",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 4,
"visitors" => 1
}
]
end
end
end

View File

@ -412,5 +412,116 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
}
]
end
@tag :ee_only
test "return revenue metrics for countries breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
country_code: "EE"
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
country_code: "EE"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
country_code: "EE"
),
build(:pageview,
user_id: 4,
country_code: "GB"
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
country_code: "GB"
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/countries#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"name" => "Estonia",
"alpha_3" => "EST",
"code" => "EE",
"flag" => "🇪🇪",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"name" => "United Kingdom",
"alpha_3" => "GBR",
"code" => "GB",
"flag" => "🇬🇧",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2,
"visitors" => 1
}
]
end
end
end

View File

@ -212,6 +212,114 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
}
]
end
@tag :ee_only
test "return revenue metrics for operating systems breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, operating_system: "Mac"),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 2, operating_system: "Mac"),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 3, operating_system: "Mac"),
build(:pageview, user_id: 4, operating_system: "Android"),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 5, operating_system: "Android"),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/operating-systems#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"conversion_rate" => 100.0,
"name" => "(not set)",
"total_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"total_visitors" => 2,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"name" => "Mac",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"name" => "Android",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2,
"visitors" => 1
}
]
end
end
describe "GET /api/stats/:domain/operating-system-versions" do

View File

@ -3009,5 +3009,371 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
}
]
end
@tag :ee_only
test "return revenue metrics for entry pages breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, pathname: "/first"),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 2, pathname: "/second"),
build(:event,
user_id: 2,
name: "Payment",
revenue_reporting_amount: Decimal.new("3000"),
revenue_reporting_currency: "USD"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("4000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 3, pathname: "/first"),
build(:event,
name: "Payment",
user_id: 3,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 4, pathname: "/third"),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("2500"),
revenue_reporting_currency: "USD"
),
build(:event, name: "Payment", revenue_reporting_amount: nil),
build(:event, name: "Payment", revenue_reporting_amount: nil)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn =
get(
conn,
"/api/stats/#{site.domain}/entry-pages#{q}"
)
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 100.0,
"name" => "/first",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 2,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$3,500.00",
"short" => "$3.5K",
"value" => 3500.0
},
"conversion_rate" => 100.0,
"name" => "/second",
"total_revenue" => %{
"currency" => "USD",
"long" => "$7,000.00",
"short" => "$7.0K",
"value" => 7000.0
},
"total_visitors" => 1,
"visitors" => 1
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$2,500.00",
"short" => "$2.5K",
"value" => 2500.0
},
"conversion_rate" => 100.0,
"name" => "/third",
"total_revenue" => %{
"currency" => "USD",
"long" => "$2,500.00",
"short" => "$2.5K",
"value" => 2500.0
},
"total_visitors" => 1,
"visitors" => 1
}
]
end
@tag :ee_only
test "return revenue metrics for exit pages breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, pathname: "/first"),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 1, pathname: "/exit_first"),
build(:pageview, user_id: 2, pathname: "/second"),
build(:event,
user_id: 2,
name: "Payment",
revenue_reporting_amount: Decimal.new("3000"),
revenue_reporting_currency: "USD"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("4000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 2, pathname: "/exit_second"),
build(:pageview, user_id: 3, pathname: "/first"),
build(:event,
name: "Payment",
user_id: 3,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 3, pathname: "/exit_first"),
build(:pageview, user_id: 4, pathname: "/third"),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("2500"),
revenue_reporting_currency: "USD"
),
build(:event, name: "Payment", revenue_reporting_amount: nil),
build(:event, name: "Payment", revenue_reporting_amount: nil)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn =
get(
conn,
"/api/stats/#{site.domain}/exit-pages#{q}"
)
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 100.0,
"name" => "/exit_first",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 2,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$3,500.00",
"short" => "$3.5K",
"value" => 3500.0
},
"conversion_rate" => 100.0,
"name" => "/exit_second",
"total_revenue" => %{
"currency" => "USD",
"long" => "$7,000.00",
"short" => "$7.0K",
"value" => 7000.0
},
"total_visitors" => 1,
"visitors" => 1
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$2,500.00",
"short" => "$2.5K",
"value" => 2500.0
},
"conversion_rate" => 100.0,
"name" => "/third",
"total_revenue" => %{
"currency" => "USD",
"long" => "$2,500.00",
"short" => "$2.5K",
"value" => 2500.0
},
"total_visitors" => 1,
"visitors" => 1
}
]
end
@tag :ee_only
test "return revenue metrics for pages breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, pathname: "/first"),
build(:event,
name: "Payment",
pathname: "/purchase/first",
user_id: 1,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 1, pathname: "/exit_first"),
build(:pageview, user_id: 2, pathname: "/second"),
build(:event,
user_id: 2,
name: "Payment",
pathname: "/purchase/second",
revenue_reporting_amount: Decimal.new("3000"),
revenue_reporting_currency: "USD"
),
build(:event,
name: "Payment",
pathname: "/purchase/second",
user_id: 2,
revenue_reporting_amount: Decimal.new("4000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 2, pathname: "/exit_second"),
build(:pageview, user_id: 3, pathname: "/first"),
build(:event,
name: "Payment",
pathname: "/purchase/first",
user_id: 3,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 3, pathname: "/exit_first"),
build(:pageview, user_id: 4, pathname: "/third"),
build(:event,
name: "Payment",
pathname: "/purchase/third",
user_id: 4,
revenue_reporting_amount: Decimal.new("2500"),
revenue_reporting_currency: "USD"
),
build(:event, name: "Payment", pathname: "/nopay", revenue_reporting_amount: nil),
build(:event, name: "Payment", pathname: "/nopay", revenue_reporting_amount: nil),
build(:event, name: "Payment", pathname: "/nopay", revenue_reporting_amount: nil)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn =
get(
conn,
"/api/stats/#{site.domain}/pages#{q}"
)
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$0.00",
"short" => "$0.0",
"value" => 0.0
},
"conversion_rate" => 100.0,
"name" => "/nopay",
"total_revenue" => %{
"currency" => "USD",
"long" => "$0.00",
"short" => "$0.0",
"value" => 0.0
},
"total_visitors" => 3,
"visitors" => 3
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 100.0,
"name" => "/purchase/first",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 2,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$3,500.00",
"short" => "$3.5K",
"value" => 3500.0
},
"conversion_rate" => 100.0,
"name" => "/purchase/second",
"total_revenue" => %{
"currency" => "USD",
"long" => "$7,000.00",
"short" => "$7.0K",
"value" => 7000.0
},
"total_visitors" => 1,
"visitors" => 1
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$2,500.00",
"short" => "$2.5K",
"value" => 2500.0
},
"conversion_rate" => 100.0,
"name" => "/purchase/third",
"total_revenue" => %{
"currency" => "USD",
"long" => "$2,500.00",
"short" => "$2.5K",
"value" => 2500.0
},
"total_visitors" => 1,
"visitors" => 1
}
]
end
end
end

View File

@ -94,5 +94,124 @@ defmodule PlausibleWeb.Api.StatsController.RegionsTest do
assert resp = response(conn, 400)
assert resp =~ "Failed to parse 'to' argument."
end
@tag :ee_only
test "return revenue metrics for regions breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
country_code: "EE",
subdivision1_code: "EE-37",
city_geoname_id: 588_409
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
country_code: "EE",
subdivision1_code: "EE-37",
city_geoname_id: 588_409
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
country_code: "EE",
subdivision1_code: "EE-37",
city_geoname_id: 588_409
),
build(:pageview,
user_id: 4,
country_code: "EE",
subdivision1_code: "EE-39",
city_geoname_id: 591_632
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
country_code: "EE",
subdivision1_code: "EE-39",
city_geoname_id: 591_632
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/regions#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 33.33,
"name" => "Harjumaa",
"code" => "EE-37",
"country_flag" => "🇪🇪",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 6,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 25.0,
"name" => "Hiiumaa",
"code" => "EE-39",
"country_flag" => "🇪🇪",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 4,
"visitors" => 1
}
]
end
end
end

View File

@ -316,5 +316,113 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
%{"name" => "Mobile", "visitors" => 1, "percentage" => 33.3}
]
end
@tag :ee_only
test "return revenue metrics for screen sizes breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, screen_size: "Mobile"),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 2, screen_size: "Mobile"),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 3, screen_size: "Mobile"),
build(:pageview, user_id: 4, screen_size: "Desktop"),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 5, screen_size: "Desktop"),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"conversion_rate" => 100.0,
"name" => "(not set)",
"total_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"total_visitors" => 2,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"name" => "Mobile",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"name" => "Desktop",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2,
"visitors" => 1
}
]
end
end
end

View File

@ -678,6 +678,139 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
"comparison_date_range_label" => "1 Jan 2021"
}
end
@tag :ee_only
test "return revenue metrics for sources breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
referrer_source: "Google",
referrer: "google.com"
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
referrer_source: "Google",
referrer: "google.com"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
referrer_source: "Google",
referrer: "google.com"
),
build(:pageview,
user_id: 4,
referrer_source: "DuckDuckGo",
referrer: "duckduckgo.com"
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
referrer_source: "DuckDuckGo",
referrer: "duckduckgo.com"
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
),
build(:pageview,
user_id: 8,
referrer_source: "Bing",
referrer: "bing.com"
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/sources#{q}")
assert json_response(conn, 200)["results"] == [
%{
"name" => "Direct / None",
"visitors" => 2,
"average_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"conversion_rate" => 100.0,
"total_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"total_visitors" => 2
},
%{
"name" => "Google",
"visitors" => 2,
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3
},
%{
"name" => "DuckDuckGo",
"visitors" => 1,
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2
}
]
end
end
describe "UTM parameters with hostname filter" do
@ -774,6 +907,134 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
end
@tag :ee_only
test "return revenue metrics for channels breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
referrer_source: "Google",
referrer: "google.com"
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
referrer_source: "Google",
referrer: "google.com"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
referrer_source: "Google",
referrer: "google.com"
),
build(:pageview,
user_id: 4,
referrer_source: "Facebook",
utm_source: "fb-ads"
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
referrer_source: "Facebook",
utm_source: "fb-ads"
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/channels#{q}")
assert json_response(conn, 200)["results"] == [
%{
"name" => "Direct",
"visitors" => 2,
"average_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"conversion_rate" => 100.0,
"total_revenue" => %{
"currency" => "USD",
"long" => "$600.00",
"short" => "$600.0",
"value" => 600.0
},
"total_visitors" => 2
},
%{
"name" => "Organic Search",
"visitors" => 2,
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3
},
%{
"name" => "Paid Social",
"visitors" => 1,
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2
}
]
end
end
describe "GET /api/stats/:domain/utm_mediums" do
@ -926,6 +1187,111 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
end
@tag :ee_only
test "return revenue metrics for UTM mediums breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
utm_medium: "social"
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
utm_medium: "social"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
utm_medium: "social"
),
build(:pageview,
user_id: 4,
utm_medium: "email"
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
utm_medium: "email"
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/utm_mediums#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"name" => "social",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"name" => "email",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2,
"visitors" => 1
}
]
end
end
describe "GET /api/stats/:domain/utm_campaigns" do
@ -1086,6 +1452,111 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
end
@tag :ee_only
test "return revenue metrics for UTM campaigns breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
utm_campaign: "profile"
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
utm_campaign: "profile"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
utm_campaign: "profile"
),
build(:pageview,
user_id: 4,
utm_campaign: "august"
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
utm_campaign: "august"
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/utm_campaigns#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"name" => "profile",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"name" => "august",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2,
"visitors" => 1
}
]
end
end
describe "GET /api/stats/:domain/utm_sources" do
@ -1134,6 +1605,111 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
end
@tag :ee_only
test "return revenue metrics for UTM sources breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
utm_source: "Twitter"
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
utm_source: "Twitter"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
utm_source: "Twitter"
),
build(:pageview,
user_id: 4,
utm_source: "newsletter"
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
utm_source: "newsletter"
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/utm_sources#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"name" => "Twitter",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"name" => "newsletter",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2,
"visitors" => 1
}
]
end
end
describe "GET /api/stats/:domain/utm_terms" do
@ -1294,6 +1870,111 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
end
@tag :ee_only
test "return revenue metrics for UTM terms breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
utm_term: "oat milk"
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
utm_term: "oat milk"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
utm_term: "oat milk"
),
build(:pageview,
user_id: 4,
utm_term: "Sweden"
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
utm_term: "Sweden"
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/utm_terms#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"name" => "oat milk",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"name" => "Sweden",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2,
"visitors" => 1
}
]
end
end
describe "GET /api/stats/:domain/utm_contents" do
@ -1454,6 +2135,111 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
end
@tag :ee_only
test "return revenue metrics for UTM contents breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,
user_id: 1,
utm_content: "ad"
),
build(:event,
name: "Payment",
user_id: 1,
revenue_reporting_amount: Decimal.new("1000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 2,
utm_content: "ad"
),
build(:event,
name: "Payment",
user_id: 2,
revenue_reporting_amount: Decimal.new("2000"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 3,
utm_content: "ad"
),
build(:pageview,
user_id: 4,
utm_content: "blog"
),
build(:event,
name: "Payment",
user_id: 4,
revenue_reporting_amount: Decimal.new("500"),
revenue_reporting_currency: "USD"
),
build(:pageview,
user_id: 5,
utm_content: "blog"
),
build(:pageview, user_id: 6),
build(:event,
name: "Payment",
user_id: 6,
revenue_reporting_amount: Decimal.new("600"),
revenue_reporting_currency: "USD"
),
build(:pageview, user_id: 7),
build(:event,
name: "Payment",
user_id: 7,
revenue_reporting_amount: nil
)
])
insert(:goal, %{site: site, event_name: "Payment", currency: :USD})
filters = Jason.encode!([[:is, "event:goal", ["Payment"]]])
order_by = Jason.encode!([["visitors", "desc"]])
q = "?filters=#{filters}&order_by=#{order_by}&detailed=true&period=day&page=1&limit=100"
conn = get(conn, "/api/stats/#{site.domain}/utm_contents#{q}")
assert json_response(conn, 200)["results"] == [
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$1,500.00",
"short" => "$1.5K",
"value" => 1500.0
},
"conversion_rate" => 66.67,
"name" => "ad",
"total_revenue" => %{
"currency" => "USD",
"long" => "$3,000.00",
"short" => "$3.0K",
"value" => 3000.0
},
"total_visitors" => 3,
"visitors" => 2
},
%{
"average_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"conversion_rate" => 50.0,
"name" => "blog",
"total_revenue" => %{
"currency" => "USD",
"long" => "$500.00",
"short" => "$500.0",
"value" => 500.0
},
"total_visitors" => 2,
"visitors" => 1
}
]
end
end
describe "GET /api/stats/:domain/sources - with goal filter" do