Revert api conversion rate (#3789)

* Revert "Unify percentage change for CR and bounce_rate (#3781)"

This reverts commit a6b1a6ebc7.

* Revert "Bring Stats API up to speed: Add `conversion_rate` to Aggregate and Breakdown (#3739)"

This reverts commit 672d682e95.
This commit is contained in:
RobertJoonas 2024-02-15 17:43:35 +00:00 committed by GitHub
parent c430274d7e
commit 8e8790dd30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 328 additions and 933 deletions

View File

@ -2,9 +2,6 @@
All notable changes to this project will be documented in this file.
### Added
- Display `Total visitors`, `Conversions`, and `CR` in the "Details" views of Countries, Regions and Cities (when filtering by a goal)
- Add `conversion_rate` to Regions and Cities reports (when filtering by a goal)
- Add the `conversion_rate` metric to Stats API Breakdown and Aggregate endpoints
- IP Block List in Site Settings
- Allow filtering with `contains`/`matches` operator for Sources, Browsers and Operating Systems.
- Allow filtering by multiple custom properties
@ -40,8 +37,6 @@ All notable changes to this project will be documented in this file.
- Replace `CLICKHOUSE_MAX_BUFFER_SIZE` with `CLICKHOUSE_MAX_BUFFER_SIZE_BYTES`
### Fixed
- Calculate `conversion_rate` percentage change in the same way like `bounce_rate` (subtraction instead of division)
- Calculate `bounce_rate` percentage change in the Stats API in the same way as it's done in the dashboard
- Stop returning custom events in goal breakdown with a pageview goal filter and vice versa
- Only return `(none)` values in custom property breakdown for the first page (pagination) of results
- Fixed weekly/monthly e-mail report [rendering issues](https://github.com/plausible/analytics/issues/284)

View File

@ -52,7 +52,7 @@ export default function Router({ site, loggedIn, currentUserRole }) {
<ExitPagesModal site={site} />
</Route>
<Route path="/:domain/countries">
<ModalTable title="Top countries" site={site} endpoint={url.apiPath(site, '/countries')} filter={{ country: 'code', country_labels: 'name' }} keyLabel="Country" renderIcon={renderCountryIcon} showPercentage={true}/>
<ModalTable title="Top countries" site={site} endpoint={url.apiPath(site, '/countries')} filter={{ country: 'code', country_labels: 'name' }} keyLabel="Country" renderIcon={renderCountryIcon} />
</Route>
<Route path="/:domain/regions">
<ModalTable title="Top regions" site={site} endpoint={url.apiPath(site, '/regions')} filter={{ region: 'code', region_labels: 'name' }} keyLabel="Region" renderIcon={renderRegionIcon} />

View File

@ -57,7 +57,7 @@ function Regions({query, site, onClick}) {
getFilterFor={getFilterFor}
onClick={onClick}
keyLabel="Region"
metrics={maybeWithCR([VISITORS_METRIC], query)}
metrics={[VISITORS_METRIC]}
detailsLink={sitePath(site, '/regions')}
query={query}
renderIcon={renderIcon}
@ -84,7 +84,7 @@ function Cities({query, site}) {
fetchData={fetchData}
getFilterFor={getFilterFor}
keyLabel="City"
metrics={maybeWithCR([VISITORS_METRIC], query)}
metrics={[VISITORS_METRIC]}
detailsLink={sitePath(site, '/cities')}
query={query}
renderIcon={renderIcon}

View File

@ -20,24 +20,8 @@ class ModalTable extends React.Component {
.then((res) => this.setState({loading: false, list: res}))
}
showConversionRate() {
return !!this.state.query.filters.goal
}
showPercentage() {
return this.props.showPercentage && !this.showConversionRate()
}
label() {
if (this.state.query.period === 'realtime') {
return 'Current visitors'
}
if (this.showConversionRate()) {
return 'Conversions'
}
return 'Visitors'
return this.state.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
}
renderTableItem(tableItem) {
@ -56,10 +40,11 @@ class ModalTable extends React.Component {
{tableItem.name}
</Link>
</td>
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{numberFormatter(tableItem.total_visitors)}</td>}
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(tableItem.visitors)}</td>
{this.showPercentage() && <td className="p-2 w-32 font-medium" align="right">{tableItem.percentage}</td>}
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{numberFormatter(tableItem.conversion_rate)}%</td>}
<td className="p-2 w-32 font-medium" align="right">
{numberFormatter(tableItem.visitors)}
{tableItem.percentage >= 0 &&
<span className="inline-block text-xs w-8 pl-1 text-right">({tableItem.percentage}%)</span> }
</td>
</tr>
)
}
@ -81,11 +66,19 @@ class ModalTable extends React.Component {
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
<thead>
<tr>
<th className="p-2 w-48 md:w-56 lg:w-1/3 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">{this.props.keyLabel}</th>
{this.showConversionRate() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right" >Total Visitors</th>}
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">{this.label()}</th>
{this.showPercentage() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">%</th>}
{this.showConversionRate() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">CR</th>}
<th
className="p-2 w-48 lg:w-1/2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="left"
>
{this.props.keyLabel}
</th>
<th
// eslint-disable-next-line max-len
className="p-2 w-32 lg:w-1/2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="right"
>
{this.label()}
</th>
</tr>
</thead>
<tbody>

View File

@ -1,9 +1,9 @@
defmodule Plausible.Stats.Aggregate do
alias Plausible.Stats.Query
use Plausible.ClickhouseRepo
use Plausible
import Plausible.Stats.{Base, Imported}
import Plausible.Stats.{Base, Imported, Util}
import Ecto.Query
alias Plausible.Stats.{Query, Util}
@revenue_metrics on_full_build(do: Plausible.Stats.Goal.Revenue.revenue_metrics(), else: [])
@ -26,13 +26,8 @@ defmodule Plausible.Stats.Aggregate do
Query.trace(query, metrics)
event_metrics =
metrics
|> Util.maybe_add_visitors_metric()
|> Enum.filter(&(&1 in @event_metrics))
event_metrics = Enum.filter(metrics, &(&1 in @event_metrics))
event_task = fn -> aggregate_events(site, query, event_metrics) end
session_metrics = Enum.filter(metrics, &(&1 in @session_metrics))
session_task = fn -> aggregate_sessions(site, query, session_metrics) end
@ -45,38 +40,12 @@ defmodule Plausible.Stats.Aggregate do
Plausible.ClickhouseRepo.parallel_tasks([session_task, event_task, time_on_page_task])
|> Enum.reduce(%{}, fn aggregate, task_result -> Map.merge(aggregate, task_result) end)
|> maybe_put_cr(site, query, metrics)
|> Util.keep_requested_metrics(metrics)
|> cast_revenue_metrics_to_money(currency)
|> Enum.map(&maybe_round_value/1)
|> Enum.map(fn {metric, value} -> {metric, %{value: value}} end)
|> Enum.into(%{})
end
defp maybe_put_cr(aggregate_result, site, query, metrics) do
if :conversion_rate in metrics do
all =
query
|> Query.remove_event_filters([:goal, :props])
|> then(fn query -> aggregate_events(site, query, [:visitors]) end)
|> Map.fetch!(:visitors)
converted = aggregate_result.visitors
cr = Util.calculate_cr(all, converted)
aggregate_result = Map.put(aggregate_result, :conversion_rate, cr)
if :total_visitors in metrics do
Map.put(aggregate_result, :total_visitors, all)
else
aggregate_result
end
else
aggregate_result
end
end
defp aggregate_events(_, _, []), do: %{}
defp aggregate_events(site, query, metrics) do
@ -94,7 +63,7 @@ defmodule Plausible.Stats.Aggregate do
|> select_session_metrics(metrics, query)
|> merge_imported(site, query, :aggregate, metrics)
|> ClickhouseRepo.one()
|> Util.keep_requested_metrics(metrics)
|> remove_internal_visits_metric()
end
defp aggregate_time_on_page(site, query) do

View File

@ -3,9 +3,9 @@ defmodule Plausible.Stats.Breakdown do
use Plausible
use Plausible.Stats.Fragments
import Plausible.Stats.{Base, Imported}
import Plausible.Stats.{Base, Imported, Util}
require OpenTelemetry.Tracer, as: Tracer
alias Plausible.Stats.{Query, Util}
alias Plausible.Stats.Query
@no_ref "Direct / None"
@not_set "(not set)"
@ -16,12 +16,7 @@ defmodule Plausible.Stats.Breakdown do
@event_metrics [:visitors, :pageviews, :events] ++ @revenue_metrics
# These metrics can be asked from the `breakdown/5` function,
# but they are different from regular metrics such as `visitors`,
# or `bounce_rate` - we cannot currently "select them" directly in
# the db queries. Instead, we need to artificially append them to
# the breakdown results later on.
@computed_metrics [:conversion_rate, :total_visitors]
@event_props Plausible.Stats.Props.event_props()
def breakdown(site, query, property, metrics, pagination, opts \\ [])
@ -34,22 +29,15 @@ defmodule Plausible.Stats.Breakdown do
if !Keyword.get(opts, :skip_tracing), do: trace(query, property, metrics)
{revenue_goals, metrics} =
if full_build?() && Plausible.Billing.Feature.RevenueGoals.enabled?(site) do
revenue_goals = Enum.filter(event_goals, &Plausible.Goal.Revenue.revenue?/1)
metrics = if Enum.empty?(revenue_goals), do: metrics -- @revenue_metrics, else: metrics
{revenue_goals, metrics}
else
{nil, metrics -- @revenue_metrics}
end
metrics_to_select = Util.maybe_add_visitors_metric(metrics) -- @computed_metrics
event_results =
if Enum.any?(event_goals) do
revenue_goals =
on_full_build do
Enum.filter(event_goals, &Plausible.Goal.Revenue.revenue?/1)
end
site
|> breakdown(event_query, "event:name", metrics_to_select, pagination, skip_tracing: true)
|> breakdown(event_query, "event:name", metrics, pagination, skip_tracing: true)
|> transform_keys(%{name: :goal})
|> cast_revenue_metrics_to_money(revenue_goals)
else
@ -80,16 +68,14 @@ defmodule Plausible.Stats.Breakdown do
goal: fragment("concat('Visit ', ?[index])", ^page_exprs)
}
)
|> select_event_metrics(metrics_to_select -- @revenue_metrics)
|> select_event_metrics(metrics -- @revenue_metrics)
|> ClickhouseRepo.all()
|> Enum.map(fn row -> Map.delete(row, :index) end)
else
[]
end
zip_results(event_results, page_results, :goal, metrics_to_select)
|> maybe_add_cr(site, query, nil, metrics)
|> Util.keep_requested_metrics(metrics)
zip_results(event_results, page_results, :goal, metrics)
end
def breakdown(site, query, "event:props:" <> custom_prop = property, metrics, pagination, opts) do
@ -100,8 +86,6 @@ defmodule Plausible.Stats.Breakdown do
{nil, metrics}
end
metrics_to_select = Util.maybe_add_visitors_metric(metrics) -- @computed_metrics
{_limit, page} = pagination
none_result =
@ -113,7 +97,7 @@ defmodule Plausible.Stats.Breakdown do
select_merge: %{^custom_prop => "(none)"},
having: fragment("uniq(?)", e.user_id) > 0
)
|> select_event_metrics(metrics_to_select)
|> select_event_metrics(metrics)
|> ClickhouseRepo.all()
else
[]
@ -121,28 +105,29 @@ defmodule Plausible.Stats.Breakdown do
if !Keyword.get(opts, :skip_tracing), do: trace(query, property, metrics)
breakdown_events(site, query, "event:props:" <> custom_prop, metrics_to_select, pagination)
breakdown_events(site, query, "event:props:" <> custom_prop, metrics, pagination)
|> Kernel.++(none_result)
|> Enum.map(&cast_revenue_metrics_to_money(&1, currency))
|> Enum.sort_by(& &1[sorting_key(metrics_to_select)], :desc)
|> maybe_add_cr(site, query, nil, metrics)
|> Util.keep_requested_metrics(metrics)
|> Enum.sort_by(& &1[sorting_key(metrics)], :desc)
end
def breakdown(site, query, "event:page" = property, metrics, pagination, opts) do
event_metrics =
metrics
|> Util.maybe_add_visitors_metric()
|> Enum.filter(&(&1 in @event_metrics))
event_metrics = Enum.filter(metrics, &(&1 in @event_metrics))
session_metrics = Enum.filter(metrics, &(&1 in @session_metrics))
event_result = breakdown_events(site, query, "event:page", event_metrics, pagination)
event_result =
site
|> breakdown_events(query, "event:page", event_metrics, pagination)
|> maybe_add_time_on_page(site, query, metrics)
|> maybe_add_cr(site, query, property, metrics)
|> Util.keep_requested_metrics(metrics)
if :time_on_page in metrics do
pages = Enum.map(event_result, & &1[:page])
time_on_page_result = breakdown_time_on_page(site, query, pages)
session_metrics = Enum.filter(metrics, &(&1 in @session_metrics))
Enum.map(event_result, fn row ->
Map.put(row, :time_on_page, time_on_page_result[row[:page]])
end)
else
event_result
end
new_query =
case event_result do
@ -176,19 +161,14 @@ defmodule Plausible.Stats.Breakdown do
end
end
def breakdown(site, query, "event:name" = property, metrics, pagination, opts) do
def breakdown(site, query, property, metrics, pagination, opts) when property in @event_props do
if !Keyword.get(opts, :skip_tracing), do: trace(query, property, metrics)
breakdown_events(site, query, property, metrics, pagination)
end
def breakdown(site, query, property, metrics, pagination, opts) do
if !Keyword.get(opts, :skip_tracing), do: trace(query, property, metrics)
metrics_to_select = Util.maybe_add_visitors_metric(metrics) -- @computed_metrics
breakdown_sessions(site, query, property, metrics_to_select, pagination)
|> maybe_add_cr(site, query, property, metrics)
|> Util.keep_requested_metrics(metrics)
breakdown_sessions(site, query, property, metrics, pagination)
end
defp zip_results(event_result, session_result, property, metrics) do
@ -231,7 +211,7 @@ defmodule Plausible.Stats.Breakdown do
|> apply_pagination(pagination)
|> ClickhouseRepo.all()
|> transform_keys(%{operating_system: :os})
|> Util.keep_requested_metrics(metrics)
|> remove_internal_visits_metric(metrics)
end
defp breakdown_events(_, _, _, [], _), do: []
@ -249,19 +229,6 @@ defmodule Plausible.Stats.Breakdown do
|> transform_keys(%{operating_system: :os})
end
defp maybe_add_time_on_page(event_results, site, query, metrics) do
if :time_on_page in metrics do
pages = Enum.map(event_results, & &1[:page])
time_on_page_result = breakdown_time_on_page(site, query, pages)
Enum.map(event_results, fn row ->
Map.put(row, :time_on_page, time_on_page_result[row[:page]])
end)
else
event_results
end
end
defp breakdown_time_on_page(_site, _query, []) do
%{}
end
@ -659,82 +626,6 @@ defmodule Plausible.Stats.Breakdown do
)
end
defp maybe_add_cr(breakdown_results, site, query, property, metrics) do
cond do
:conversion_rate not in metrics -> breakdown_results
Enum.empty?(breakdown_results) -> breakdown_results
is_nil(property) -> add_absolute_cr(breakdown_results, site, query)
true -> add_cr(breakdown_results, site, query, property, metrics)
end
end
# This function injects a conversion_rate metric into every
# breakdown result map. It is calculated as X / Y, where:
#
# * X is the number of conversions for a breakdown
# result (conversion = number of visitors who
# completed the filtered goal with the filtered
# custom properties).
#
# * Y is the number of all visitors for this breakdown
# result without the `event:goal` and `event:props:*`
# filters.
defp add_cr(breakdown_results, site, query, property, metrics) do
property_atom = Plausible.Stats.Filters.without_prefix(property)
items =
Enum.map(breakdown_results, fn item -> Map.fetch!(item, property_atom) end)
query_without_goal =
query
|> Query.put_filter(property, {:member, items})
|> Query.remove_event_filters([:goal, :props])
# Here, we're always only interested in the first page of results
# - the :member filter makes sure that the results always match with
# the items in the given breakdown_results list
pagination = {length(items), 1}
res_without_goal = breakdown(site, query_without_goal, property, [:visitors], pagination)
Enum.map(breakdown_results, fn item ->
without_goal =
Enum.find(res_without_goal, fn s ->
Map.fetch!(s, property_atom) == Map.fetch!(item, property_atom)
end)
item =
item
|> Map.put(:conversion_rate, Util.calculate_cr(without_goal.visitors, item.visitors))
if :total_visitors in metrics do
Map.put(item, :total_visitors, without_goal.visitors)
else
item
end
end)
end
# Similar to `add_cr/5`, injects a conversion_rate metric into
# every breakdown result. However, a single divisor is used in
# the CR calculation across all breakdown results. That is the
# number of visitors without `event:goal` and `event:props:*`
# filters.
#
# This is useful when we're only interested in the conversions
# themselves - not how well a certain property such as browser
# or page converted.
defp add_absolute_cr(breakdown_results, site, query) do
total_q = Query.remove_event_filters(query, [:goal, :props])
%{visitors: %{value: total_visitors}} = Plausible.Stats.aggregate(site, total_q, [:visitors])
breakdown_results
|> Enum.map(fn goal ->
Map.put(goal, :conversion_rate, Util.calculate_cr(total_visitors, goal[:visitors]))
end)
end
defp sorting_key(metrics) do
if Enum.member?(metrics, :visitors), do: :visitors, else: List.first(metrics)
end

View File

@ -1,8 +1,4 @@
defmodule Plausible.Stats.Compare do
def calculate_change(:conversion_rate, old_value, new_value) do
Float.round(new_value - old_value, 1)
end
def calculate_change(:bounce_rate, old_count, new_count) do
if old_count > 0, do: new_count - old_count
end

View File

@ -6,30 +6,34 @@ defmodule Plausible.Stats.Filters do
alias Plausible.Stats.Filters.{DashboardFilterParser, StatsAPIFilterParser}
@visit_props [
:source,
:referrer,
:utm_medium,
:utm_source,
:utm_campaign,
:utm_content,
:utm_term,
:screen,
:device,
:browser,
:browser_version,
:os,
:os_version,
:country,
:region,
:city,
:entry_page,
:exit_page
"source",
"referrer",
"utm_medium",
"utm_source",
"utm_campaign",
"utm_content",
"utm_term",
"screen",
"device",
"browser",
"browser_version",
"os",
"os_version",
"country",
"region",
"city",
"entry_page",
"exit_page"
]
def visit_props(), do: @visit_props |> Enum.map(&to_string/1)
def visit_props(), do: @visit_props
@event_props [:name, :page, :goal]
@event_props [
"name",
"page",
"goal"
]
def event_props(), do: @event_props |> Enum.map(&to_string/1)
def event_props(), do: @event_props
@doc """
Parses different filter formats.
@ -63,11 +67,4 @@ defmodule Plausible.Stats.Filters do
def parse(filters) when is_map(filters), do: DashboardFilterParser.parse_and_prefix(filters)
def parse(_), do: %{}
def without_prefix(property) do
property
|> String.split(":")
|> List.last()
|> String.to_existing_atom()
end
end

View File

@ -1,8 +1,8 @@
defmodule Plausible.Stats.Timeseries do
use Plausible.ClickhouseRepo
use Plausible
alias Plausible.Stats.{Query, Util}
import Plausible.Stats.{Base}
alias Plausible.Stats.Query
import Plausible.Stats.{Base, Util}
use Plausible.Stats.Fragments
@typep metric ::
@ -69,7 +69,7 @@ defmodule Plausible.Stats.Timeseries do
|> select_session_metrics(metrics, query)
|> Plausible.Stats.Imported.merge_imported_timeseries(site, query, metrics)
|> ClickhouseRepo.all()
|> Util.keep_requested_metrics(metrics)
|> remove_internal_visits_metric(metrics)
end
defp buckets(%Query{interval: "month"} = query) do

View File

@ -3,59 +3,21 @@ defmodule Plausible.Stats.Util do
Utilities for modifying stat results
"""
@manually_removable_metrics [:__internal_visits, :visitors]
@doc """
Sometimes we need to manually add metrics in order to calculate the value for
other metrics. E.g:
* `__internal_visits` is fetched when querying bounce rate and visit duration,
as it is needed to calculate these from imported data.
* `visitors` metric might be added manually via `maybe_add_visitors_metric/1`,
in order to be able to calculate conversion rate.
This function can be used for stripping those metrics from a breakdown (list),
or an aggregate (map) result. We do not want to return metrics that we're not
requested.
`__internal_visits` is fetched when querying bounce rate and visit duration, as it
is needed to calculate these from imported data. This function removes that metric
from all entries in the results list.
"""
def keep_requested_metrics(results, requested_metrics) when is_list(results) do
Enum.map(results, fn results_map ->
keep_requested_metrics(results_map, requested_metrics)
end)
end
def keep_requested_metrics(results, requested_metrics) do
Map.drop(results, @manually_removable_metrics -- requested_metrics)
end
@doc """
This function adds the `visitors` metric into the list of
given metrics if it's not already there and if there is a
`conversion_rate` metric in the list.
Currently, the conversion rate cannot be queried from the
database with a simple select clause - instead, we need to
fetch the database result first, and then manually add it
into the aggregate map or every entry of thebreakdown list.
In order for us to be able to calculate it based on the
results returned by the database query, the visitors metric
needs to be queried.
"""
def maybe_add_visitors_metric(metrics) do
if :conversion_rate in metrics and :visitors not in metrics do
metrics ++ [:visitors]
def remove_internal_visits_metric(results, metrics) when is_list(results) do
if :bounce_rate in metrics or :visit_duration in metrics do
results
|> Enum.map(&remove_internal_visits_metric/1)
else
metrics
results
end
end
def calculate_cr(nil, _converted_visitors), do: nil
def calculate_cr(unique_visitors, converted_visitors) do
if unique_visitors > 0,
do: Float.round(converted_visitors / unique_visitors * 100, 1),
else: 0.0
def remove_internal_visits_metric(result) when is_map(result) do
Map.delete(result, :__internal_visits)
end
end

View File

@ -2,20 +2,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
use PlausibleWeb, :controller
use Plausible.Repo
use PlausibleWeb.Plugs.ErrorHandler
alias Plausible.Stats.{Query, Compare, Comparisons}
@metrics [
:visitors,
:visits,
:pageviews,
:views_per_visit,
:bounce_rate,
:visit_duration,
:events,
:conversion_rate
]
@metric_mappings Enum.into(@metrics, %{}, fn metric -> {to_string(metric), metric} end)
alias Plausible.Stats.Query
def realtime_visitors(conn, _params) do
site = conn.assigns.site
@ -34,7 +21,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
:ok <- ensure_custom_props_access(site, query) do
results =
if params["compare"] == "previous_period" do
{:ok, prev_query} = Comparisons.compare(site, query, "previous_period")
{:ok, prev_query} = Plausible.Stats.Comparisons.compare(site, query, "previous_period")
[prev_result, curr_result] =
Plausible.ClickhouseRepo.parallel_tasks([
@ -44,9 +31,12 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
Enum.map(curr_result, fn {metric, %{value: current_val}} ->
%{value: prev_val} = prev_result[metric]
change = Compare.calculate_change(metric, prev_val, current_val)
{metric, %{value: current_val, change: change}}
{metric,
%{
value: current_val,
change: percent_change(prev_val, current_val)
}}
end)
|> Enum.into(%{})
else
@ -106,6 +96,13 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
@default_breakdown_limit 100
defp validate_or_default_limit(_), do: {:ok, @default_breakdown_limit}
defp event_only_property?("event:name"), do: true
defp event_only_property?("event:goal"), do: true
defp event_only_property?("event:props:" <> _), do: true
defp event_only_property?(_), do: false
@event_metrics ["visitors", "pageviews", "events"]
@session_metrics ["visits", "bounce_rate", "visit_duration", "views_per_visit"]
defp parse_and_validate_metrics(params, property, query) do
metrics =
Map.get(params, "metrics", "visitors")
@ -116,7 +113,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
{:error, reason}
metrics ->
{:ok, Enum.map(metrics, &Map.fetch!(@metric_mappings, &1))}
{:ok, Enum.map(metrics, &String.to_existing_atom/1)}
end
end
@ -158,61 +155,26 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
end)
end
defp validate_metric("conversion_rate" = metric, property, query) do
defp validate_metric("events", nil, %{include_imported: true}) do
{:error, "Metric `events` cannot be queried with imported data"}
end
defp validate_metric(metric, _, _) when metric in @event_metrics, do: {:ok, metric}
defp validate_metric(metric, property, query) when metric in @session_metrics do
event_only_filter = Map.keys(query.filters) |> Enum.find(&event_only_property?/1)
cond do
property == "event:goal" ->
{:ok, metric}
query.filters["event:goal"] ->
{:ok, metric}
true ->
{:error,
"Metric `#{metric}` can only be queried in a goal breakdown or with a goal filter"}
end
end
defp validate_metric("events" = metric, _, query) do
if query.include_imported do
{:error, "Metric `#{metric}` cannot be queried with imported data"}
else
{:ok, metric}
end
end
defp validate_metric(metric, _, _) when metric in ["visitors", "pageviews"] do
{:ok, metric}
end
defp validate_metric("views_per_visit" = metric, property, query) do
cond do
query.filters["event:page"] ->
metric == "views_per_visit" && query.filters["event:page"] ->
{:error, "Metric `#{metric}` cannot be queried with a filter on `event:page`."}
property != nil ->
metric == "views_per_visit" && property != nil ->
{:error, "Metric `#{metric}` is not supported in breakdown queries."}
true ->
validate_session_metric(metric, property, query)
end
end
defp validate_metric(metric, property, query)
when metric in ["visits", "bounce_rate", "visit_duration"] do
validate_session_metric(metric, property, query)
end
defp validate_metric(metric, _, _) do
{:error,
"The metric `#{metric}` is not recognized. Find valid metrics from the documentation: https://plausible.io/docs/stats-api#metrics"}
end
defp validate_session_metric(metric, property, query) do
cond do
event_only_property?(property) ->
{:error, "Session metric `#{metric}` cannot be queried for breakdown by `#{property}`."}
event_only_filter = find_event_only_filter(query) ->
event_only_filter ->
{:error,
"Session metric `#{metric}` cannot be queried when using a filter on `#{event_only_filter}`."}
@ -221,15 +183,11 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
end
end
defp find_event_only_filter(query) do
Map.keys(query.filters) |> Enum.find(&event_only_property?/1)
defp validate_metric(metric, _, _) do
{:error,
"The metric `#{metric}` is not recognized. Find valid metrics from the documentation: https://plausible.io/docs/stats-api#metrics"}
end
defp event_only_property?("event:name"), do: true
defp event_only_property?("event:goal"), do: true
defp event_only_property?("event:props:" <> _), do: true
defp event_only_property?(_), do: false
def timeseries(conn, params) do
site = Repo.preload(conn.assigns.site, :owner)
@ -248,6 +206,19 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
end
end
defp percent_change(old_count, new_count) do
cond do
old_count == 0 and new_count > 0 ->
100
old_count == 0 and new_count == 0 ->
0
true ->
round((new_count - old_count) / old_count * 100)
end
end
defp validate_date(%{"period" => "custom"} = params) do
with {:ok, date} <- Map.fetch(params, "date"),
[from, to] <- String.split(date, ","),

View File

@ -346,23 +346,61 @@ defmodule PlausibleWeb.Api.StatsController do
end
defp fetch_top_stats(site, %Query{filters: %{"event:goal" => _}} = query, comparison_query) do
metrics =
[:total_visitors, :visitors, :events, :conversion_rate] ++ @revenue_metrics
query_without_filters = Query.remove_event_filters(query, [:goal, :props])
metrics = [:visitors, :events] ++ @revenue_metrics
results = Stats.aggregate(site, query, metrics)
comparison = if comparison_query, do: Stats.aggregate(site, comparison_query, metrics)
results_without_filters =
site
|> Stats.aggregate(query_without_filters, [:visitors])
|> transform_keys(%{visitors: :unique_visitors})
results =
site
|> Stats.aggregate(query, metrics)
|> transform_keys(%{visitors: :converted_visitors, events: :completions})
|> Map.merge(results_without_filters)
comparison =
if comparison_query do
comparison_query_without_filters =
Query.remove_event_filters(comparison_query, [:goal, :props])
comparison_without_filters =
site
|> Stats.aggregate(comparison_query_without_filters, [:visitors])
|> transform_keys(%{visitors: :unique_visitors})
site
|> Stats.aggregate(comparison_query, metrics)
|> transform_keys(%{visitors: :converted_visitors, events: :completions})
|> Map.merge(comparison_without_filters)
end
conversion_rate = %{
cr: %{value: calculate_cr(results.unique_visitors.value, results.converted_visitors.value)}
}
comparison_conversion_rate =
if comparison do
value =
calculate_cr(comparison.unique_visitors.value, comparison.converted_visitors.value)
%{cr: %{value: value}}
else
nil
end
[
top_stats_entry(results, comparison, "Unique visitors", :total_visitors),
top_stats_entry(results, comparison, "Unique conversions", :visitors),
top_stats_entry(results, comparison, "Total conversions", :events),
top_stats_entry(results, comparison, "Unique visitors", :unique_visitors),
top_stats_entry(results, comparison, "Unique conversions", :converted_visitors),
top_stats_entry(results, comparison, "Total conversions", :completions),
on_full_build do
top_stats_entry(results, comparison, "Average revenue", :average_revenue, &format_money/1)
end,
on_full_build do
top_stats_entry(results, comparison, "Total revenue", :total_revenue, &format_money/1)
end,
top_stats_entry(results, comparison, "Conversion rate", :conversion_rate)
top_stats_entry(conversion_rate, comparison_conversion_rate, "Conversion rate", :cr)
]
|> Enum.reject(&is_nil/1)
|> then(&{&1, 100})
@ -431,16 +469,17 @@ defmodule PlausibleWeb.Api.StatsController do
def sources(conn, params) do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics)
metrics =
if params["detailed"], do: [:visitors, :bounce_rate, :visit_duration], else: [:visitors]
res =
Stats.breakdown(site, query, "visit:source", metrics, pagination)
|> add_cr(site, query, pagination, :source, "visit:source")
|> transform_keys(%{source: :name})
if params["csv"] do
@ -513,12 +552,16 @@ defmodule PlausibleWeb.Api.StatsController do
def utm_mediums(conn, params) do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics = [:visitors, :bounce_rate, :visit_duration]
res =
Stats.breakdown(site, query, "visit:utm_medium", metrics, pagination)
|> add_cr(site, query, pagination, :utm_medium, "visit:utm_medium")
|> transform_keys(%{utm_medium: :name})
if params["csv"] do
@ -536,12 +579,16 @@ defmodule PlausibleWeb.Api.StatsController do
def utm_campaigns(conn, params) do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics = [:visitors, :bounce_rate, :visit_duration]
res =
Stats.breakdown(site, query, "visit:utm_campaign", metrics, pagination)
|> add_cr(site, query, pagination, :utm_campaign, "visit:utm_campaign")
|> transform_keys(%{utm_campaign: :name})
if params["csv"] do
@ -559,12 +606,15 @@ defmodule PlausibleWeb.Api.StatsController do
def utm_contents(conn, params) do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics = [:visitors, :bounce_rate, :visit_duration]
res =
Stats.breakdown(site, query, "visit:utm_content", metrics, pagination)
|> add_cr(site, query, pagination, :utm_content, "visit:utm_content")
|> transform_keys(%{utm_content: :name})
if params["csv"] do
@ -582,12 +632,15 @@ defmodule PlausibleWeb.Api.StatsController do
def utm_terms(conn, params) do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics = [:visitors, :bounce_rate, :visit_duration]
res =
Stats.breakdown(site, query, "visit:utm_term", metrics, pagination)
|> add_cr(site, query, pagination, :utm_term, "visit:utm_term")
|> transform_keys(%{utm_term: :name})
if params["csv"] do
@ -605,12 +658,16 @@ defmodule PlausibleWeb.Api.StatsController do
def utm_sources(conn, params) do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics = [:visitors, :bounce_rate, :visit_duration]
res =
Stats.breakdown(site, query, "visit:utm_source", metrics, pagination)
|> add_cr(site, query, pagination, :utm_source, "visit:utm_source")
|> transform_keys(%{utm_source: :name})
if params["csv"] do
@ -628,12 +685,16 @@ defmodule PlausibleWeb.Api.StatsController do
def referrers(conn, params) do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
metrics = [:visitors, :bounce_rate, :visit_duration]
res =
Stats.breakdown(site, query, "visit:referrer", metrics, pagination)
|> add_cr(site, query, pagination, :referrer, "visit:referrer")
|> transform_keys(%{referrer: :name})
if params["csv"] do
@ -693,13 +754,12 @@ defmodule PlausibleWeb.Api.StatsController do
pagination = parse_pagination(params)
extra_metrics =
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
metrics = breakdown_metrics(query, extra_metrics)
metrics =
if params["detailed"], do: [:visitors, :bounce_rate, :visit_duration], else: [:visitors]
referrers =
Stats.breakdown(site, query, "visit:referrer", metrics, pagination)
|> add_cr(site, query, pagination, :referrer, "visit:referrer")
|> transform_keys(%{referrer: :name})
json(conn, referrers)
@ -709,16 +769,16 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = Query.from(site, params)
extra_metrics =
metrics =
if params["detailed"],
do: [:pageviews, :bounce_rate, :time_on_page],
else: []
do: [:visitors, :pageviews, :bounce_rate, :time_on_page],
else: [:visitors]
metrics = breakdown_metrics(query, extra_metrics)
pagination = parse_pagination(params)
pages =
Stats.breakdown(site, query, "event:page", metrics, pagination)
|> add_cr(site, query, pagination, :page, "event:page")
|> transform_keys(%{page: :name})
if params["csv"] do
@ -738,10 +798,11 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query, [:visits, :visit_duration])
metrics = [:visitors, :visits, :visit_duration]
entry_pages =
Stats.breakdown(site, query, "visit:entry_page", metrics, pagination)
|> add_cr(site, query, pagination, :entry_page, "visit:entry_page")
|> transform_keys(%{entry_page: :name})
if params["csv"] do
@ -768,10 +829,11 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = Query.from(site, params)
{limit, page} = parse_pagination(params)
metrics = breakdown_metrics(query, [:visits])
metrics = [:visitors, :visits]
exit_pages =
Stats.breakdown(site, query, "visit:exit_page", metrics, {limit, page})
|> add_cr(site, query, {limit, page}, :exit_page, "visit:exit_page")
|> add_exit_rate(site, query, limit)
|> transform_keys(%{exit_page: :name})
@ -827,10 +889,10 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = site |> Query.from(params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
countries =
Stats.breakdown(site, query, "visit:country", metrics, pagination)
Stats.breakdown(site, query, "visit:country", [:visitors], pagination)
|> add_cr(site, query, {300, 1}, :country, "visit:country")
|> transform_keys(%{country: :code})
|> add_percentages(site, query)
@ -879,10 +941,9 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = site |> Query.from(params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
regions =
Stats.breakdown(site, query, "visit:region", metrics, pagination)
Stats.breakdown(site, query, "visit:region", [:visitors], pagination)
|> transform_keys(%{region: :code})
|> Enum.map(fn region ->
region_entry = Location.get_subdivision(region[:code])
@ -913,10 +974,9 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = site |> Query.from(params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
cities =
Stats.breakdown(site, query, "visit:city", metrics, pagination)
Stats.breakdown(site, query, "visit:city", [:visitors], pagination)
|> transform_keys(%{city: :code})
|> Enum.map(fn city ->
city_info = Location.get_city(city[:code])
@ -952,10 +1012,10 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
browsers =
Stats.breakdown(site, query, "visit:browser", metrics, pagination)
Stats.breakdown(site, query, "visit:browser", [:visitors], pagination)
|> add_cr(site, query, pagination, :browser, "visit:browser")
|> transform_keys(%{browser: :name})
|> add_percentages(site, query)
@ -976,10 +1036,10 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
versions =
Stats.breakdown(site, query, "visit:browser_version", metrics, pagination)
Stats.breakdown(site, query, "visit:browser_version", [:visitors], pagination)
|> add_cr(site, query, pagination, :browser_version, "visit:browser_version")
|> transform_keys(%{browser_version: :name})
|> add_percentages(site, query)
@ -1006,10 +1066,10 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
systems =
Stats.breakdown(site, query, "visit:os", metrics, pagination)
Stats.breakdown(site, query, "visit:os", [:visitors], pagination)
|> add_cr(site, query, pagination, :os, "visit:os")
|> transform_keys(%{os: :name})
|> add_percentages(site, query)
@ -1030,10 +1090,10 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
versions =
Stats.breakdown(site, query, "visit:os_version", metrics, pagination)
Stats.breakdown(site, query, "visit:os_version", [:visitors], pagination)
|> add_cr(site, query, pagination, :os_version, "visit:os_version")
|> transform_keys(%{os_version: :name})
|> add_percentages(site, query)
@ -1044,10 +1104,10 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site]
query = Query.from(site, params)
pagination = parse_pagination(params)
metrics = breakdown_metrics(query)
sizes =
Stats.breakdown(site, query, "visit:device", metrics, pagination)
Stats.breakdown(site, query, "visit:device", [:visitors], pagination)
|> add_cr(site, query, pagination, :device, "visit:device")
|> transform_keys(%{device: :name})
|> add_percentages(site, query)
@ -1064,6 +1124,14 @@ defmodule PlausibleWeb.Api.StatsController do
end
end
defp calculate_cr(nil, _converted_visitors), do: nil
defp calculate_cr(unique_visitors, converted_visitors) do
if unique_visitors > 0,
do: Float.round(converted_visitors / unique_visitors * 100, 1),
else: 0.0
end
def conversions(conn, params) do
pagination = parse_pagination(params)
site = Plausible.Repo.preload(conn.assigns.site, :goals)
@ -1076,7 +1144,21 @@ defmodule PlausibleWeb.Api.StatsController do
query
end
metrics = [:visitors, :events, :conversion_rate] ++ @revenue_metrics
total_q = Query.remove_event_filters(query, [:goal, :props])
%{visitors: %{value: total_visitors}} = Stats.aggregate(site, total_q, [:visitors])
metrics =
on_full_build do
if Enum.any?(site.goals, &Plausible.Goal.Revenue.revenue?/1) and
Plausible.Billing.Feature.RevenueGoals.enabled?(site) do
[:visitors, :events] ++ @revenue_metrics
else
[:visitors, :events]
end
else
[:visitors, :events]
end
conversions =
site
@ -1084,6 +1166,7 @@ defmodule PlausibleWeb.Api.StatsController do
|> transform_keys(%{goal: :name})
|> Enum.map(fn goal ->
goal
|> Map.put(:conversion_rate, calculate_cr(total_visitors, goal[:visitors]))
|> Enum.map(&format_revenue_metric/1)
|> Map.new()
end)
@ -1157,19 +1240,32 @@ defmodule PlausibleWeb.Api.StatsController do
|> Map.put(:include_imported, false)
metrics =
if query.filters["event:goal"] do
[:visitors, :events, :conversion_rate] ++ @revenue_metrics
else
if full_build?() and Map.has_key?(query.filters, "event:goal") do
[:visitors, :events] ++ @revenue_metrics
else
[:visitors, :events]
end
Stats.breakdown(site, query, prefixed_prop, metrics, pagination)
|> transform_keys(%{prop_key => :name})
|> Enum.map(fn entry ->
Enum.map(entry, &format_revenue_metric/1)
|> Map.new()
end)
|> add_percentages(site, query)
props =
Stats.breakdown(site, query, prefixed_prop, metrics, pagination)
|> transform_keys(%{prop_key => :name})
|> Enum.map(fn entry ->
Enum.map(entry, &format_revenue_metric/1)
|> Map.new()
end)
|> add_percentages(site, query)
if Map.has_key?(query.filters, "event:goal") do
total_q = Query.remove_event_filters(query, [:goal, :props])
%{visitors: %{value: total_unique_visitors}} = Stats.aggregate(site, total_q, [:visitors])
Enum.map(props, fn prop ->
Map.put(prop, :conversion_rate, calculate_cr(total_unique_visitors, prop.visitors))
end)
else
props
end
end
def current_visitors(conn, _) do
@ -1228,6 +1324,37 @@ defmodule PlausibleWeb.Api.StatsController do
defp add_percentages(breakdown_result, _, _), do: breakdown_result
defp add_cr([_ | _] = breakdown_results, site, query, pagination, key_name, filter_name)
when is_map_key(query.filters, "event:goal") do
items = Enum.map(breakdown_results, fn item -> Map.fetch!(item, key_name) end)
query_without_goal =
query
|> Query.put_filter(filter_name, {:member, items})
|> Query.remove_event_filters([:goal, :props])
# Here, we're always only interested in the first page of results
# - the :member filter makes sure that the results always match with
# the items in the given breakdown_results list
pagination = {elem(pagination, 0), 1}
res_without_goal =
Stats.breakdown(site, query_without_goal, filter_name, [:visitors], pagination)
Enum.map(breakdown_results, fn item ->
without_goal =
Enum.find(res_without_goal, fn s ->
Map.fetch!(s, key_name) == Map.fetch!(item, key_name)
end)
item
|> Map.put(:total_visitors, without_goal.visitors)
|> Map.put(:conversion_rate, calculate_cr(without_goal.visitors, item.visitors))
end)
end
defp add_cr(breakdown_results, _, _, _, _, _), do: breakdown_results
defp to_csv(list, columns), do: to_csv(list, columns, columns)
defp to_csv(list, columns, column_names) do
@ -1358,12 +1485,4 @@ defmodule PlausibleWeb.Api.StatsController do
{metric, value}
end
end
defp breakdown_metrics(query, extra_metrics \\ []) do
if query.filters["event:goal"] do
[:visitors, :conversion_rate, :total_visitors]
else
[:visitors] ++ extra_metrics
end
end
end

View File

@ -140,22 +140,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
end
end
test "validates that conversion_rate cannot be queried without a goal filter", %{
conn: conn,
site: site
} do
conn =
get(conn, "/api/v1/stats/aggregate", %{
"site_id" => site.domain,
"metrics" => "conversion_rate"
})
assert %{"error" => msg} = json_response(conn, 400)
assert msg =~
"Metric `conversion_rate` can only be queried in a goal breakdown or with a goal filter"
end
test "validates that views_per_visit cannot be used with event:page filter", %{
conn: conn,
site: site
@ -172,23 +156,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
"Metric `views_per_visit` cannot be queried with a filter on `event:page`."
}
end
test "validates that views_per_visit cannot be used with an event only filter", %{
conn: conn,
site: site
} do
conn =
get(conn, "/api/v1/stats/aggregate", %{
"site_id" => site.domain,
"filters" => "event:name==Something",
"metrics" => "views_per_visit"
})
assert json_response(conn, 400) == %{
"error" =>
"Session metric `views_per_visit` cannot be queried when using a filter on `event:name`."
}
end
end
test "aggregates a single metric", %{conn: conn, site: site} do
@ -322,39 +289,10 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
assert json_response(conn, 200)["results"] == %{
"pageviews" => %{"value" => 4, "change" => 100},
"visitors" => %{"value" => 3, "change" => 100},
"bounce_rate" => %{"value" => 100, "change" => nil},
"bounce_rate" => %{"value" => 100, "change" => 100},
"visit_duration" => %{"value" => 0, "change" => 0}
}
end
test "can compare conversion_rate with previous period", %{conn: conn, site: site} do
today = ~N[2023-05-05 12:00:00]
yesterday = Timex.shift(today, days: -1)
populate_stats(site, [
build(:event, name: "Signup", timestamp: yesterday),
build(:pageview, timestamp: yesterday),
build(:pageview, timestamp: yesterday),
build(:event, name: "Signup", timestamp: today),
build(:pageview, timestamp: today)
])
insert(:goal, %{site: site, event_name: "Signup"})
conn =
get(conn, "/api/v1/stats/aggregate", %{
"site_id" => site.domain,
"period" => "day",
"date" => "2023-05-05",
"metrics" => "conversion_rate",
"filters" => "event:goal==Signup",
"compare" => "previous_period"
})
assert json_response(conn, 200)["results"] == %{
"conversion_rate" => %{"value" => 50.0, "change" => 16.7}
}
end
end
describe "with imported data" do
@ -419,7 +357,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
"visitors" => %{"value" => 2, "change" => 100},
"visits" => %{"value" => 5, "change" => 150},
"pageviews" => %{"value" => 9, "change" => -10},
"bounce_rate" => %{"value" => 40, "change" => -10},
"bounce_rate" => %{"value" => 40, "change" => -20},
"views_per_visit" => %{"value" => 1.0, "change" => 100},
"visit_duration" => %{"value" => 20, "change" => -80}
}
@ -1296,111 +1234,4 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
assert json_response(conn, 200)["results"] == %{"pageviews" => %{"value" => 3}}
end
end
describe "metrics" do
test "conversion_rate when goal filter is applied", %{conn: conn, site: site} do
populate_stats(site, [
build(:event, name: "Signup"),
build(:pageview)
])
insert(:goal, %{site: site, event_name: "Signup"})
conn =
get(conn, "/api/v1/stats/aggregate", %{
"site_id" => site.domain,
"metrics" => "conversion_rate",
"filters" => "event:goal==Signup"
})
assert json_response(conn, 200)["results"] == %{"conversion_rate" => %{"value" => 50}}
end
test "conversion_rate when goal + custom prop filter applied", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event, name: "Signup"),
build(:event, name: "Signup", "meta.key": ["author"], "meta.value": ["Uku"]),
build(:event, name: "Signup", "meta.key": ["author"], "meta.value": ["Marko"]),
build(:pageview)
])
insert(:goal, %{site: site, event_name: "Signup"})
conn =
get(conn, "/api/v1/stats/aggregate", %{
"site_id" => site.domain,
"metrics" => "conversion_rate,visitors,events",
"filters" => "event:goal==Signup;event:props:author==Uku"
})
assert %{
"conversion_rate" => %{"value" => 25.0},
"visitors" => %{"value" => 1},
"events" => %{"value" => 1}
} = json_response(conn, 200)["results"]
end
test "conversion_rate when goal + visit property filter applied", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event, name: "Signup"),
build(:event, name: "Signup", browser: "Chrome"),
build(:event, name: "Signup", browser: "Firefox", user_id: 123),
build(:event, name: "Signup", browser: "Firefox", user_id: 123),
build(:pageview, browser: "Firefox"),
build(:pageview)
])
insert(:goal, %{site: site, event_name: "Signup"})
conn =
get(conn, "/api/v1/stats/aggregate", %{
"site_id" => site.domain,
"metrics" => "conversion_rate,visitors,events",
"filters" => "visit:browser==Firefox;event:goal==Signup"
})
assert %{
"conversion_rate" => %{"value" => 50.0},
"visitors" => %{"value" => 1},
"events" => %{"value" => 2}
} =
json_response(conn, 200)["results"]
end
test "conversion_rate when goal + page filter applied", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event, name: "Signup"),
build(:event, name: "Signup", pathname: "/not-this"),
build(:event, name: "Signup", pathname: "/this", user_id: 123),
build(:event, name: "Signup", pathname: "/this", user_id: 123),
build(:pageview, pathname: "/this"),
build(:pageview)
])
insert(:goal, %{site: site, event_name: "Signup"})
conn =
get(conn, "/api/v1/stats/aggregate", %{
"site_id" => site.domain,
"metrics" => "conversion_rate,visitors,events",
"filters" => "event:page==/this;event:goal==Signup"
})
assert %{
"conversion_rate" => %{"value" => 50.0},
"visitors" => %{"value" => 1},
"events" => %{"value" => 2}
} =
json_response(conn, 200)["results"]
end
end
end

View File

@ -83,23 +83,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
end
describe "param validation" do
test "does not allow querying conversion_rate without a goal filter", %{
conn: conn,
site: site
} do
conn =
get(conn, "/api/v1/stats/breakdown", %{
"site_id" => site.domain,
"property" => "event:page",
"metrics" => "conversion_rate"
})
assert json_response(conn, 400) == %{
"error" =>
"Metric `conversion_rate` can only be queried in a goal breakdown or with a goal filter"
}
end
test "validates that property is required", %{conn: conn, site: site} do
conn =
get(conn, "/api/v1/stats/breakdown", %{
@ -2018,308 +2001,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
end
describe "metrics" do
test "returns conversion_rate in an event:goal breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:event, name: "Signup", user_id: 1),
build(:event, name: "Signup", user_id: 1),
build(:pageview, pathname: "/blog"),
build(:pageview, pathname: "/blog/post"),
build(:pageview)
])
insert(:goal, %{site: site, event_name: "Signup"})
insert(:goal, %{site: site, page_path: "/blog**"})
conn =
get(conn, "/api/v1/stats/breakdown", %{
"site_id" => site.domain,
"period" => "day",
"property" => "event:goal",
"metrics" => "visitors,events,conversion_rate"
})
assert json_response(conn, 200) == %{
"results" => [
%{
"goal" => "Visit /blog**",
"visitors" => 2,
"events" => 2,
"conversion_rate" => 50
},
%{
"goal" => "Signup",
"visitors" => 1,
"events" => 2,
"conversion_rate" => 25
}
]
}
end
test "returns conversion_rate alone in an event:goal breakdown", %{conn: conn, site: site} do
populate_stats(site, [
build(:event, name: "Signup", user_id: 1),
build(:pageview)
])
insert(:goal, %{site: site, event_name: "Signup"})
conn =
get(conn, "/api/v1/stats/breakdown", %{
"site_id" => site.domain,
"period" => "day",
"property" => "event:goal",
"metrics" => "conversion_rate"
})
assert json_response(conn, 200) == %{
"results" => [
%{
"goal" => "Signup",
"conversion_rate" => 50
}
]
}
end
test "returns conversion_rate in a goal filtered custom prop breakdown", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:pageview, pathname: "/blog/1", "meta.key": ["author"], "meta.value": ["Uku"]),
build(:pageview, pathname: "/blog/2", "meta.key": ["author"], "meta.value": ["Uku"]),
build(:pageview, pathname: "/blog/3", "meta.key": ["author"], "meta.value": ["Uku"]),
build(:pageview, pathname: "/blog/1", "meta.key": ["author"], "meta.value": ["Marko"]),
build(:pageview,
pathname: "/blog/2",
"meta.key": ["author"],
"meta.value": ["Marko"],
user_id: 1
),
build(:pageview,
pathname: "/blog/3",
"meta.key": ["author"],
"meta.value": ["Marko"],
user_id: 1
),
build(:pageview, pathname: "/blog"),
build(:pageview, "meta.key": ["author"], "meta.value": ["Marko"]),
build(:pageview)
])
insert(:goal, %{site: site, page_path: "/blog**"})
conn =
get(conn, "/api/v1/stats/breakdown", %{
"site_id" => site.domain,
"period" => "day",
"property" => "event:props:author",
"filters" => "event:goal==Visit /blog**",
"metrics" => "visitors,events,conversion_rate"
})
assert json_response(conn, 200) == %{
"results" => [
%{
"author" => "Uku",
"visitors" => 3,
"events" => 3,
"conversion_rate" => 37.5
},
%{
"author" => "Marko",
"visitors" => 2,
"events" => 3,
"conversion_rate" => 25
},
%{
"author" => "(none)",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 12.5
}
]
}
end
test "returns conversion_rate alone in a goal filtered custom prop breakdown", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:pageview, pathname: "/blog/1", "meta.key": ["author"], "meta.value": ["Uku"]),
build(:pageview)
])
insert(:goal, %{site: site, page_path: "/blog**"})
conn =
get(conn, "/api/v1/stats/breakdown", %{
"site_id" => site.domain,
"period" => "day",
"property" => "event:props:author",
"filters" => "event:goal==Visit /blog**",
"metrics" => "conversion_rate"
})
assert json_response(conn, 200) == %{
"results" => [
%{
"author" => "Uku",
"conversion_rate" => 50
}
]
}
end
test "returns conversion_rate in a goal filtered event:page breakdown", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event, pathname: "/en/register"),
build(:event, pathname: "/en/register", name: "Signup"),
build(:event, pathname: "/en/register", name: "Signup"),
build(:event, pathname: "/it/register", name: "Signup", user_id: 1),
build(:event, pathname: "/it/register", name: "Signup", user_id: 1),
build(:event, pathname: "/it/register")
])
insert(:goal, %{site: site, event_name: "Signup"})
conn =
get(conn, "/api/v1/stats/breakdown", %{
"site_id" => site.domain,
"period" => "day",
"property" => "event:page",
"filters" => "event:goal==Signup",
"metrics" => "visitors,events,conversion_rate"
})
assert json_response(conn, 200) == %{
"results" => [
%{
"page" => "/en/register",
"visitors" => 2,
"events" => 2,
"conversion_rate" => 66.7
},
%{
"page" => "/it/register",
"visitors" => 1,
"events" => 2,
"conversion_rate" => 50
}
]
}
end
test "returns conversion_rate alone in a goal filtered event:page breakdown", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event, pathname: "/en/register"),
build(:event, pathname: "/en/register", name: "Signup")
])
insert(:goal, %{site: site, event_name: "Signup"})
conn =
get(conn, "/api/v1/stats/breakdown", %{
"site_id" => site.domain,
"period" => "day",
"property" => "event:page",
"filters" => "event:goal==Signup",
"metrics" => "conversion_rate"
})
assert json_response(conn, 200) == %{
"results" => [
%{
"page" => "/en/register",
"conversion_rate" => 50
}
]
}
end
test "returns conversion_rate in a multi-goal filtered visit:screen_size breakdown", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event, screen_size: "Mobile"),
build(:event, screen_size: "Mobile", name: "AddToCart"),
build(:event, screen_size: "Mobile", name: "AddToCart"),
build(:event, screen_size: "Desktop", name: "AddToCart", user_id: 1),
build(:event, screen_size: "Desktop", name: "Purchase", user_id: 1),
build(:event, screen_size: "Desktop")
])
# Make sure that revenue goals are treated the same
# way as regular custom event goals
insert(:goal, %{site: site, event_name: "Purchase", currency: :EUR})
insert(:goal, %{site: site, event_name: "AddToCart"})
conn =
get(conn, "/api/v1/stats/breakdown", %{
"site_id" => site.domain,
"period" => "day",
"property" => "visit:device",
"filters" => "event:goal==AddToCart|Purchase",
"metrics" => "visitors,events,conversion_rate"
})
assert json_response(conn, 200) == %{
"results" => [
%{
"device" => "Mobile",
"visitors" => 2,
"events" => 2,
"conversion_rate" => 66.7
},
%{
"device" => "Desktop",
"visitors" => 1,
"events" => 2,
"conversion_rate" => 50
}
]
}
end
test "returns conversion_rate alone in a goal filtered visit:screen_size breakdown", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event, screen_size: "Mobile"),
build(:event, screen_size: "Mobile", name: "AddToCart")
])
insert(:goal, %{site: site, event_name: "AddToCart"})
conn =
get(conn, "/api/v1/stats/breakdown", %{
"site_id" => site.domain,
"period" => "day",
"property" => "visit:device",
"filters" => "event:goal==AddToCart",
"metrics" => "conversion_rate"
})
assert json_response(conn, 200) == %{
"results" => [
%{
"device" => "Mobile",
"conversion_rate" => 50
}
]
}
end
test "all metrics for breakdown by visit prop", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,

View File

@ -1093,22 +1093,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.TimeseriesTest do
end
describe "metrics" do
test "validates that conversion_rate cannot be queried without a goal filter", %{
conn: conn,
site: site
} do
conn =
get(conn, "/api/v1/stats/timeseries", %{
"site_id" => site.domain,
"metrics" => "conversion_rate"
})
assert %{"error" => msg} = json_response(conn, 400)
assert msg ==
"Metric `conversion_rate` can only be queried in a goal breakdown or with a goal filter"
end
test "shows pageviews,visits,views_per_visit for last 7d", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview,

View File

@ -1313,13 +1313,17 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
%{
"total_visitors" => 2,
"visitors" => 1,
"visits" => 1,
"name" => "/page1",
"visit_duration" => 0,
"conversion_rate" => 50.0
},
%{
"total_visitors" => 1,
"visitors" => 1,
"visits" => 1,
"name" => "/page2",
"visit_duration" => 900,
"conversion_rate" => 100.0
}
]
@ -1504,12 +1508,14 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
"name" => "/exit1",
"visitors" => 1,
"total_visitors" => 1,
"visits" => 1,
"conversion_rate" => 100.0
},
%{
"name" => "/exit2",
"visitors" => 1,
"total_visitors" => 1,
"visits" => 1,
"conversion_rate" => 100.0
}
]

View File

@ -1094,7 +1094,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
res = json_response(conn, 200)
assert %{
"change" => -33.4,
"change" => -50,
"comparison_value" => 66.7,
"name" => "Conversion rate",
"value" => 33.3