From a2ba1256d214ceec68d21a8f8e55a467c498801b Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 18 Nov 2025 12:24:54 +0100 Subject: [PATCH] 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 --- .../modals/devices/browser-versions-modal.js | 2 +- .../stats/modals/devices/browsers-modal.js | 2 +- .../stats/modals/devices/choose-metrics.js | 12 +- .../operating-system-versions-modal.js | 2 +- .../modals/devices/operating-systems-modal.js | 2 +- .../stats/modals/devices/screen-sizes.js | 2 +- .../js/dashboard/stats/modals/entry-pages.js | 11 +- .../js/dashboard/stats/modals/exit-pages.js | 11 +- .../dashboard/stats/modals/locations-modal.js | 11 +- assets/js/dashboard/stats/modals/pages.js | 11 +- .../stats/modals/referrer-drilldown.js | 11 +- assets/js/dashboard/stats/modals/sources.js | 11 +- lib/plausible/stats/sql/expression.ex | 43 + lib/plausible/stats/sql/query_builder.ex | 37 +- lib/plausible/stats/table_decider.ex | 4 + .../controllers/api/stats_controller.ex | 134 ++- .../api/stats_controller/browsers_test.exs | 108 +++ .../api/stats_controller/cities_test.exs | 119 +++ .../api/stats_controller/countries_test.exs | 111 +++ .../operating_systems_test.exs | 108 +++ .../api/stats_controller/pages_test.exs | 366 ++++++++ .../api/stats_controller/regions_test.exs | 119 +++ .../stats_controller/screen_sizes_test.exs | 108 +++ .../api/stats_controller/sources_test.exs | 786 ++++++++++++++++++ 24 files changed, 2081 insertions(+), 50 deletions(-) diff --git a/assets/js/dashboard/stats/modals/devices/browser-versions-modal.js b/assets/js/dashboard/stats/modals/devices/browser-versions-modal.js index 7fa8bfb48f..f95ead72f6 100644 --- a/assets/js/dashboard/stats/modals/devices/browser-versions-modal.js +++ b/assets/js/dashboard/stats/modals/devices/browser-versions-modal.js @@ -52,7 +52,7 @@ function BrowserVersionsModal() { 'Conversions', width: 'w-28' }), - metrics.createConversionRate() - ] + metrics.createConversionRate(), + showRevenueMetrics && metrics.createTotalRevenue(), + showRevenueMetrics && metrics.createAverageRevenue() + ].filter((metric) => !!metric) } if (isRealTimeDashboard(query)) { diff --git a/assets/js/dashboard/stats/modals/devices/operating-system-versions-modal.js b/assets/js/dashboard/stats/modals/devices/operating-system-versions-modal.js index 0749cc84de..782bb10bb4 100644 --- a/assets/js/dashboard/stats/modals/devices/operating-system-versions-modal.js +++ b/assets/js/dashboard/stats/modals/devices/operating-system-versions-modal.js @@ -49,7 +49,7 @@ function OperatingSystemVersionsModal() { 'Conversions', width: 'w-28' }), - metrics.createConversionRate() - ] + metrics.createConversionRate(), + showRevenueMetrics && metrics.createTotalRevenue(), + showRevenueMetrics && metrics.createAverageRevenue() + ].filter((metric) => !!metric) } if (isRealTimeDashboard(query)) { diff --git a/assets/js/dashboard/stats/modals/exit-pages.js b/assets/js/dashboard/stats/modals/exit-pages.js index 039efadbd7..0cd5dc34dc 100644 --- a/assets/js/dashboard/stats/modals/exit-pages.js +++ b/assets/js/dashboard/stats/modals/exit-pages.js @@ -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') { diff --git a/assets/js/dashboard/stats/modals/locations-modal.js b/assets/js/dashboard/stats/modals/locations-modal.js index b81ea88c7b..cbb898d497 100644 --- a/assets/js/dashboard/stats/modals/locations-modal.js +++ b/assets/js/dashboard/stats/modals/locations-modal.js @@ -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') { diff --git a/assets/js/dashboard/stats/modals/pages.js b/assets/js/dashboard/stats/modals/pages.js index 18c679c938..84467dc6ea 100644 --- a/assets/js/dashboard/stats/modals/pages.js +++ b/assets/js/dashboard/stats/modals/pages.js @@ -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)) { diff --git a/assets/js/dashboard/stats/modals/referrer-drilldown.js b/assets/js/dashboard/stats/modals/referrer-drilldown.js index c47a9396be..d75bba5e91 100644 --- a/assets/js/dashboard/stats/modals/referrer-drilldown.js +++ b/assets/js/dashboard/stats/modals/referrer-drilldown.js @@ -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)) { diff --git a/assets/js/dashboard/stats/modals/sources.js b/assets/js/dashboard/stats/modals/sources.js index 9b992a7a73..619c70ac9d 100644 --- a/assets/js/dashboard/stats/modals/sources.js +++ b/assets/js/dashboard/stats/modals/sources.js @@ -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)) { diff --git a/lib/plausible/stats/sql/expression.ex b/lib/plausible/stats/sql/expression.ex index f03f487d58..70812911a3 100644 --- a/lib/plausible/stats/sql/expression.ex +++ b/lib/plausible/stats/sql/expression.ex @@ -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)) diff --git a/lib/plausible/stats/sql/query_builder.ex b/lib/plausible/stats/sql/query_builder.ex index 51a538c172..71736f5192 100644 --- a/lib/plausible/stats/sql/query_builder.ex +++ b/lib/plausible/stats/sql/query_builder.ex @@ -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 diff --git a/lib/plausible/stats/table_decider.ex b/lib/plausible/stats/table_decider.ex index 25bd03c386..e08ae206f6 100644 --- a/lib/plausible/stats/table_decider.ex +++ b/lib/plausible/stats/table_decider.ex @@ -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() diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 468eebb177..b434939f59 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -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 diff --git a/test/plausible_web/controllers/api/stats_controller/browsers_test.exs b/test/plausible_web/controllers/api/stats_controller/browsers_test.exs index 07ef195a71..18870325ff 100644 --- a/test/plausible_web/controllers/api/stats_controller/browsers_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/browsers_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/stats_controller/cities_test.exs b/test/plausible_web/controllers/api/stats_controller/cities_test.exs index ec86dee3a0..7a15769327 100644 --- a/test/plausible_web/controllers/api/stats_controller/cities_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/cities_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/stats_controller/countries_test.exs b/test/plausible_web/controllers/api/stats_controller/countries_test.exs index cd298ec7b7..71ba96043e 100644 --- a/test/plausible_web/controllers/api/stats_controller/countries_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/countries_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/stats_controller/operating_systems_test.exs b/test/plausible_web/controllers/api/stats_controller/operating_systems_test.exs index 55ff3baf30..ea064b4eb5 100644 --- a/test/plausible_web/controllers/api/stats_controller/operating_systems_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/operating_systems_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/stats_controller/pages_test.exs b/test/plausible_web/controllers/api/stats_controller/pages_test.exs index 066a3eadbc..a184938cb7 100644 --- a/test/plausible_web/controllers/api/stats_controller/pages_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/pages_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/stats_controller/regions_test.exs b/test/plausible_web/controllers/api/stats_controller/regions_test.exs index b3ba4a238d..0e94c63164 100644 --- a/test/plausible_web/controllers/api/stats_controller/regions_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/regions_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/stats_controller/screen_sizes_test.exs b/test/plausible_web/controllers/api/stats_controller/screen_sizes_test.exs index d5795f0dde..c239735757 100644 --- a/test/plausible_web/controllers/api/stats_controller/screen_sizes_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/screen_sizes_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/stats_controller/sources_test.exs b/test/plausible_web/controllers/api/stats_controller/sources_test.exs index db3a6bd2f6..98d20753a7 100644 --- a/test/plausible_web/controllers/api/stats_controller/sources_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/sources_test.exs @@ -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