mirror of
https://github.com/plausible/analytics.git
synced 2024-11-22 18:52:38 +03:00
Revert api conversion rate (#3789)
* Revert "Unify percentage change for CR and bounce_rate (#3781)" This reverts commita6b1a6ebc7
. * Revert "Bring Stats API up to speed: Add `conversion_rate` to Aggregate and Breakdown (#3739)" This reverts commit672d682e95
.
This commit is contained in:
parent
c430274d7e
commit
8e8790dd30
@ -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)
|
||||
|
@ -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} />
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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, ","),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user