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.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Added
|
### 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
|
- IP Block List in Site Settings
|
||||||
- Allow filtering with `contains`/`matches` operator for Sources, Browsers and Operating Systems.
|
- Allow filtering with `contains`/`matches` operator for Sources, Browsers and Operating Systems.
|
||||||
- Allow filtering by multiple custom properties
|
- 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`
|
- Replace `CLICKHOUSE_MAX_BUFFER_SIZE` with `CLICKHOUSE_MAX_BUFFER_SIZE_BYTES`
|
||||||
|
|
||||||
### Fixed
|
### 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
|
- 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
|
- 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)
|
- 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} />
|
<ExitPagesModal site={site} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/:domain/countries">
|
<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>
|
||||||
<Route path="/:domain/regions">
|
<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} />
|
<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}
|
getFilterFor={getFilterFor}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
keyLabel="Region"
|
keyLabel="Region"
|
||||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
metrics={[VISITORS_METRIC]}
|
||||||
detailsLink={sitePath(site, '/regions')}
|
detailsLink={sitePath(site, '/regions')}
|
||||||
query={query}
|
query={query}
|
||||||
renderIcon={renderIcon}
|
renderIcon={renderIcon}
|
||||||
@ -84,7 +84,7 @@ function Cities({query, site}) {
|
|||||||
fetchData={fetchData}
|
fetchData={fetchData}
|
||||||
getFilterFor={getFilterFor}
|
getFilterFor={getFilterFor}
|
||||||
keyLabel="City"
|
keyLabel="City"
|
||||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
metrics={[VISITORS_METRIC]}
|
||||||
detailsLink={sitePath(site, '/cities')}
|
detailsLink={sitePath(site, '/cities')}
|
||||||
query={query}
|
query={query}
|
||||||
renderIcon={renderIcon}
|
renderIcon={renderIcon}
|
||||||
|
@ -20,24 +20,8 @@ class ModalTable extends React.Component {
|
|||||||
.then((res) => this.setState({loading: false, list: res}))
|
.then((res) => this.setState({loading: false, list: res}))
|
||||||
}
|
}
|
||||||
|
|
||||||
showConversionRate() {
|
|
||||||
return !!this.state.query.filters.goal
|
|
||||||
}
|
|
||||||
|
|
||||||
showPercentage() {
|
|
||||||
return this.props.showPercentage && !this.showConversionRate()
|
|
||||||
}
|
|
||||||
|
|
||||||
label() {
|
label() {
|
||||||
if (this.state.query.period === 'realtime') {
|
return this.state.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||||
return 'Current visitors'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showConversionRate()) {
|
|
||||||
return 'Conversions'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Visitors'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTableItem(tableItem) {
|
renderTableItem(tableItem) {
|
||||||
@ -56,10 +40,11 @@ class ModalTable extends React.Component {
|
|||||||
{tableItem.name}
|
{tableItem.name}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</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">
|
||||||
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(tableItem.visitors)}</td>
|
{numberFormatter(tableItem.visitors)}
|
||||||
{this.showPercentage() && <td className="p-2 w-32 font-medium" align="right">{tableItem.percentage}</td>}
|
{tableItem.percentage >= 0 &&
|
||||||
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{numberFormatter(tableItem.conversion_rate)}%</td>}
|
<span className="inline-block text-xs w-8 pl-1 text-right">({tableItem.percentage}%)</span> }
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -81,11 +66,19 @@ class ModalTable extends React.Component {
|
|||||||
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
|
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
<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>}
|
className="p-2 w-48 lg:w-1/2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
|
||||||
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">{this.label()}</th>
|
align="left"
|
||||||
{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>}
|
{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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
defmodule Plausible.Stats.Aggregate do
|
defmodule Plausible.Stats.Aggregate do
|
||||||
|
alias Plausible.Stats.Query
|
||||||
use Plausible.ClickhouseRepo
|
use Plausible.ClickhouseRepo
|
||||||
use Plausible
|
use Plausible
|
||||||
import Plausible.Stats.{Base, Imported}
|
import Plausible.Stats.{Base, Imported, Util}
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
alias Plausible.Stats.{Query, Util}
|
|
||||||
|
|
||||||
@revenue_metrics on_full_build(do: Plausible.Stats.Goal.Revenue.revenue_metrics(), else: [])
|
@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)
|
Query.trace(query, metrics)
|
||||||
|
|
||||||
event_metrics =
|
event_metrics = Enum.filter(metrics, &(&1 in @event_metrics))
|
||||||
metrics
|
|
||||||
|> Util.maybe_add_visitors_metric()
|
|
||||||
|> Enum.filter(&(&1 in @event_metrics))
|
|
||||||
|
|
||||||
event_task = fn -> aggregate_events(site, query, event_metrics) end
|
event_task = fn -> aggregate_events(site, query, event_metrics) end
|
||||||
|
|
||||||
session_metrics = Enum.filter(metrics, &(&1 in @session_metrics))
|
session_metrics = Enum.filter(metrics, &(&1 in @session_metrics))
|
||||||
session_task = fn -> aggregate_sessions(site, query, session_metrics) end
|
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])
|
Plausible.ClickhouseRepo.parallel_tasks([session_task, event_task, time_on_page_task])
|
||||||
|> Enum.reduce(%{}, fn aggregate, task_result -> Map.merge(aggregate, task_result) end)
|
|> 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)
|
|> cast_revenue_metrics_to_money(currency)
|
||||||
|> Enum.map(&maybe_round_value/1)
|
|> Enum.map(&maybe_round_value/1)
|
||||||
|> Enum.map(fn {metric, value} -> {metric, %{value: value}} end)
|
|> Enum.map(fn {metric, value} -> {metric, %{value: value}} end)
|
||||||
|> Enum.into(%{})
|
|> Enum.into(%{})
|
||||||
end
|
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(_, _, []), do: %{}
|
||||||
|
|
||||||
defp aggregate_events(site, query, metrics) do
|
defp aggregate_events(site, query, metrics) do
|
||||||
@ -94,7 +63,7 @@ defmodule Plausible.Stats.Aggregate do
|
|||||||
|> select_session_metrics(metrics, query)
|
|> select_session_metrics(metrics, query)
|
||||||
|> merge_imported(site, query, :aggregate, metrics)
|
|> merge_imported(site, query, :aggregate, metrics)
|
||||||
|> ClickhouseRepo.one()
|
|> ClickhouseRepo.one()
|
||||||
|> Util.keep_requested_metrics(metrics)
|
|> remove_internal_visits_metric()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp aggregate_time_on_page(site, query) do
|
defp aggregate_time_on_page(site, query) do
|
||||||
|
@ -3,9 +3,9 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
use Plausible
|
use Plausible
|
||||||
use Plausible.Stats.Fragments
|
use Plausible.Stats.Fragments
|
||||||
|
|
||||||
import Plausible.Stats.{Base, Imported}
|
import Plausible.Stats.{Base, Imported, Util}
|
||||||
require OpenTelemetry.Tracer, as: Tracer
|
require OpenTelemetry.Tracer, as: Tracer
|
||||||
alias Plausible.Stats.{Query, Util}
|
alias Plausible.Stats.Query
|
||||||
|
|
||||||
@no_ref "Direct / None"
|
@no_ref "Direct / None"
|
||||||
@not_set "(not set)"
|
@not_set "(not set)"
|
||||||
@ -16,12 +16,7 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
|
|
||||||
@event_metrics [:visitors, :pageviews, :events] ++ @revenue_metrics
|
@event_metrics [:visitors, :pageviews, :events] ++ @revenue_metrics
|
||||||
|
|
||||||
# These metrics can be asked from the `breakdown/5` function,
|
@event_props Plausible.Stats.Props.event_props()
|
||||||
# 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]
|
|
||||||
|
|
||||||
def breakdown(site, query, property, metrics, pagination, opts \\ [])
|
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)
|
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 =
|
event_results =
|
||||||
if Enum.any?(event_goals) do
|
if Enum.any?(event_goals) do
|
||||||
|
revenue_goals =
|
||||||
|
on_full_build do
|
||||||
|
Enum.filter(event_goals, &Plausible.Goal.Revenue.revenue?/1)
|
||||||
|
end
|
||||||
|
|
||||||
site
|
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})
|
|> transform_keys(%{name: :goal})
|
||||||
|> cast_revenue_metrics_to_money(revenue_goals)
|
|> cast_revenue_metrics_to_money(revenue_goals)
|
||||||
else
|
else
|
||||||
@ -80,16 +68,14 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
goal: fragment("concat('Visit ', ?[index])", ^page_exprs)
|
goal: fragment("concat('Visit ', ?[index])", ^page_exprs)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> select_event_metrics(metrics_to_select -- @revenue_metrics)
|
|> select_event_metrics(metrics -- @revenue_metrics)
|
||||||
|> ClickhouseRepo.all()
|
|> ClickhouseRepo.all()
|
||||||
|> Enum.map(fn row -> Map.delete(row, :index) end)
|
|> Enum.map(fn row -> Map.delete(row, :index) end)
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
zip_results(event_results, page_results, :goal, metrics_to_select)
|
zip_results(event_results, page_results, :goal, metrics)
|
||||||
|> maybe_add_cr(site, query, nil, metrics)
|
|
||||||
|> Util.keep_requested_metrics(metrics)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def breakdown(site, query, "event:props:" <> custom_prop = property, metrics, pagination, opts) do
|
def breakdown(site, query, "event:props:" <> custom_prop = property, metrics, pagination, opts) do
|
||||||
@ -100,8 +86,6 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
{nil, metrics}
|
{nil, metrics}
|
||||||
end
|
end
|
||||||
|
|
||||||
metrics_to_select = Util.maybe_add_visitors_metric(metrics) -- @computed_metrics
|
|
||||||
|
|
||||||
{_limit, page} = pagination
|
{_limit, page} = pagination
|
||||||
|
|
||||||
none_result =
|
none_result =
|
||||||
@ -113,7 +97,7 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
select_merge: %{^custom_prop => "(none)"},
|
select_merge: %{^custom_prop => "(none)"},
|
||||||
having: fragment("uniq(?)", e.user_id) > 0
|
having: fragment("uniq(?)", e.user_id) > 0
|
||||||
)
|
)
|
||||||
|> select_event_metrics(metrics_to_select)
|
|> select_event_metrics(metrics)
|
||||||
|> ClickhouseRepo.all()
|
|> ClickhouseRepo.all()
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
@ -121,28 +105,29 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
|
|
||||||
if !Keyword.get(opts, :skip_tracing), do: trace(query, property, metrics)
|
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)
|
|> Kernel.++(none_result)
|
||||||
|> Enum.map(&cast_revenue_metrics_to_money(&1, currency))
|
|> Enum.map(&cast_revenue_metrics_to_money(&1, currency))
|
||||||
|> Enum.sort_by(& &1[sorting_key(metrics_to_select)], :desc)
|
|> Enum.sort_by(& &1[sorting_key(metrics)], :desc)
|
||||||
|> maybe_add_cr(site, query, nil, metrics)
|
|
||||||
|> Util.keep_requested_metrics(metrics)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def breakdown(site, query, "event:page" = property, metrics, pagination, opts) do
|
def breakdown(site, query, "event:page" = property, metrics, pagination, opts) do
|
||||||
event_metrics =
|
event_metrics = Enum.filter(metrics, &(&1 in @event_metrics))
|
||||||
metrics
|
session_metrics = Enum.filter(metrics, &(&1 in @session_metrics))
|
||||||
|> Util.maybe_add_visitors_metric()
|
|
||||||
|> Enum.filter(&(&1 in @event_metrics))
|
event_result = breakdown_events(site, query, "event:page", event_metrics, pagination)
|
||||||
|
|
||||||
event_result =
|
event_result =
|
||||||
site
|
if :time_on_page in metrics do
|
||||||
|> breakdown_events(query, "event:page", event_metrics, pagination)
|
pages = Enum.map(event_result, & &1[:page])
|
||||||
|> maybe_add_time_on_page(site, query, metrics)
|
time_on_page_result = breakdown_time_on_page(site, query, pages)
|
||||||
|> maybe_add_cr(site, query, property, metrics)
|
|
||||||
|> Util.keep_requested_metrics(metrics)
|
|
||||||
|
|
||||||
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 =
|
new_query =
|
||||||
case event_result do
|
case event_result do
|
||||||
@ -176,19 +161,14 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
end
|
end
|
||||||
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)
|
if !Keyword.get(opts, :skip_tracing), do: trace(query, property, metrics)
|
||||||
breakdown_events(site, query, property, metrics, pagination)
|
breakdown_events(site, query, property, metrics, pagination)
|
||||||
end
|
end
|
||||||
|
|
||||||
def breakdown(site, query, property, metrics, pagination, opts) do
|
def breakdown(site, query, property, metrics, pagination, opts) do
|
||||||
if !Keyword.get(opts, :skip_tracing), do: trace(query, property, metrics)
|
if !Keyword.get(opts, :skip_tracing), do: trace(query, property, metrics)
|
||||||
|
breakdown_sessions(site, query, property, metrics, pagination)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp zip_results(event_result, session_result, property, metrics) do
|
defp zip_results(event_result, session_result, property, metrics) do
|
||||||
@ -231,7 +211,7 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
|> apply_pagination(pagination)
|
|> apply_pagination(pagination)
|
||||||
|> ClickhouseRepo.all()
|
|> ClickhouseRepo.all()
|
||||||
|> transform_keys(%{operating_system: :os})
|
|> transform_keys(%{operating_system: :os})
|
||||||
|> Util.keep_requested_metrics(metrics)
|
|> remove_internal_visits_metric(metrics)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp breakdown_events(_, _, _, [], _), do: []
|
defp breakdown_events(_, _, _, [], _), do: []
|
||||||
@ -249,19 +229,6 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
|> transform_keys(%{operating_system: :os})
|
|> transform_keys(%{operating_system: :os})
|
||||||
end
|
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
|
defp breakdown_time_on_page(_site, _query, []) do
|
||||||
%{}
|
%{}
|
||||||
end
|
end
|
||||||
@ -659,82 +626,6 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
)
|
)
|
||||||
end
|
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
|
defp sorting_key(metrics) do
|
||||||
if Enum.member?(metrics, :visitors), do: :visitors, else: List.first(metrics)
|
if Enum.member?(metrics, :visitors), do: :visitors, else: List.first(metrics)
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
defmodule Plausible.Stats.Compare do
|
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
|
def calculate_change(:bounce_rate, old_count, new_count) do
|
||||||
if old_count > 0, do: new_count - old_count
|
if old_count > 0, do: new_count - old_count
|
||||||
end
|
end
|
||||||
|
@ -6,30 +6,34 @@ defmodule Plausible.Stats.Filters do
|
|||||||
alias Plausible.Stats.Filters.{DashboardFilterParser, StatsAPIFilterParser}
|
alias Plausible.Stats.Filters.{DashboardFilterParser, StatsAPIFilterParser}
|
||||||
|
|
||||||
@visit_props [
|
@visit_props [
|
||||||
:source,
|
"source",
|
||||||
:referrer,
|
"referrer",
|
||||||
:utm_medium,
|
"utm_medium",
|
||||||
:utm_source,
|
"utm_source",
|
||||||
:utm_campaign,
|
"utm_campaign",
|
||||||
:utm_content,
|
"utm_content",
|
||||||
:utm_term,
|
"utm_term",
|
||||||
:screen,
|
"screen",
|
||||||
:device,
|
"device",
|
||||||
:browser,
|
"browser",
|
||||||
:browser_version,
|
"browser_version",
|
||||||
:os,
|
"os",
|
||||||
:os_version,
|
"os_version",
|
||||||
:country,
|
"country",
|
||||||
:region,
|
"region",
|
||||||
:city,
|
"city",
|
||||||
:entry_page,
|
"entry_page",
|
||||||
:exit_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 """
|
@doc """
|
||||||
Parses different filter formats.
|
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(filters) when is_map(filters), do: DashboardFilterParser.parse_and_prefix(filters)
|
||||||
def parse(_), do: %{}
|
def parse(_), do: %{}
|
||||||
|
|
||||||
def without_prefix(property) do
|
|
||||||
property
|
|
||||||
|> String.split(":")
|
|
||||||
|> List.last()
|
|
||||||
|> String.to_existing_atom()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
defmodule Plausible.Stats.Timeseries do
|
defmodule Plausible.Stats.Timeseries do
|
||||||
use Plausible.ClickhouseRepo
|
use Plausible.ClickhouseRepo
|
||||||
use Plausible
|
use Plausible
|
||||||
alias Plausible.Stats.{Query, Util}
|
alias Plausible.Stats.Query
|
||||||
import Plausible.Stats.{Base}
|
import Plausible.Stats.{Base, Util}
|
||||||
use Plausible.Stats.Fragments
|
use Plausible.Stats.Fragments
|
||||||
|
|
||||||
@typep metric ::
|
@typep metric ::
|
||||||
@ -69,7 +69,7 @@ defmodule Plausible.Stats.Timeseries do
|
|||||||
|> select_session_metrics(metrics, query)
|
|> select_session_metrics(metrics, query)
|
||||||
|> Plausible.Stats.Imported.merge_imported_timeseries(site, query, metrics)
|
|> Plausible.Stats.Imported.merge_imported_timeseries(site, query, metrics)
|
||||||
|> ClickhouseRepo.all()
|
|> ClickhouseRepo.all()
|
||||||
|> Util.keep_requested_metrics(metrics)
|
|> remove_internal_visits_metric(metrics)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp buckets(%Query{interval: "month"} = query) do
|
defp buckets(%Query{interval: "month"} = query) do
|
||||||
|
@ -3,59 +3,21 @@ defmodule Plausible.Stats.Util do
|
|||||||
Utilities for modifying stat results
|
Utilities for modifying stat results
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@manually_removable_metrics [:__internal_visits, :visitors]
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sometimes we need to manually add metrics in order to calculate the value for
|
`__internal_visits` is fetched when querying bounce rate and visit duration, as it
|
||||||
other metrics. E.g:
|
is needed to calculate these from imported data. This function removes that metric
|
||||||
|
from all entries in the results list.
|
||||||
* `__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.
|
|
||||||
"""
|
"""
|
||||||
def keep_requested_metrics(results, requested_metrics) when is_list(results) do
|
def remove_internal_visits_metric(results, metrics) when is_list(results) do
|
||||||
Enum.map(results, fn results_map ->
|
if :bounce_rate in metrics or :visit_duration in metrics do
|
||||||
keep_requested_metrics(results_map, requested_metrics)
|
results
|
||||||
end)
|
|> Enum.map(&remove_internal_visits_metric/1)
|
||||||
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]
|
|
||||||
else
|
else
|
||||||
metrics
|
results
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_cr(nil, _converted_visitors), do: nil
|
def remove_internal_visits_metric(result) when is_map(result) do
|
||||||
|
Map.delete(result, :__internal_visits)
|
||||||
def calculate_cr(unique_visitors, converted_visitors) do
|
|
||||||
if unique_visitors > 0,
|
|
||||||
do: Float.round(converted_visitors / unique_visitors * 100, 1),
|
|
||||||
else: 0.0
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,20 +2,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
|||||||
use PlausibleWeb, :controller
|
use PlausibleWeb, :controller
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
use PlausibleWeb.Plugs.ErrorHandler
|
use PlausibleWeb.Plugs.ErrorHandler
|
||||||
alias Plausible.Stats.{Query, Compare, Comparisons}
|
alias Plausible.Stats.Query
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
def realtime_visitors(conn, _params) do
|
def realtime_visitors(conn, _params) do
|
||||||
site = conn.assigns.site
|
site = conn.assigns.site
|
||||||
@ -34,7 +21,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
|||||||
:ok <- ensure_custom_props_access(site, query) do
|
:ok <- ensure_custom_props_access(site, query) do
|
||||||
results =
|
results =
|
||||||
if params["compare"] == "previous_period" do
|
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] =
|
[prev_result, curr_result] =
|
||||||
Plausible.ClickhouseRepo.parallel_tasks([
|
Plausible.ClickhouseRepo.parallel_tasks([
|
||||||
@ -44,9 +31,12 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
|||||||
|
|
||||||
Enum.map(curr_result, fn {metric, %{value: current_val}} ->
|
Enum.map(curr_result, fn {metric, %{value: current_val}} ->
|
||||||
%{value: prev_val} = prev_result[metric]
|
%{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)
|
end)
|
||||||
|> Enum.into(%{})
|
|> Enum.into(%{})
|
||||||
else
|
else
|
||||||
@ -106,6 +96,13 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
|||||||
@default_breakdown_limit 100
|
@default_breakdown_limit 100
|
||||||
defp validate_or_default_limit(_), do: {:ok, @default_breakdown_limit}
|
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
|
defp parse_and_validate_metrics(params, property, query) do
|
||||||
metrics =
|
metrics =
|
||||||
Map.get(params, "metrics", "visitors")
|
Map.get(params, "metrics", "visitors")
|
||||||
@ -116,7 +113,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
|||||||
{:error, reason}
|
{:error, reason}
|
||||||
|
|
||||||
metrics ->
|
metrics ->
|
||||||
{:ok, Enum.map(metrics, &Map.fetch!(@metric_mappings, &1))}
|
{:ok, Enum.map(metrics, &String.to_existing_atom/1)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -158,61 +155,26 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
|||||||
end)
|
end)
|
||||||
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
|
cond do
|
||||||
property == "event:goal" ->
|
metric == "views_per_visit" && query.filters["event:page"] ->
|
||||||
{: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"] ->
|
|
||||||
{:error, "Metric `#{metric}` cannot be queried with a filter on `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."}
|
{: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) ->
|
event_only_property?(property) ->
|
||||||
{:error, "Session metric `#{metric}` cannot be queried for breakdown by `#{property}`."}
|
{:error, "Session metric `#{metric}` cannot be queried for breakdown by `#{property}`."}
|
||||||
|
|
||||||
event_only_filter = find_event_only_filter(query) ->
|
event_only_filter ->
|
||||||
{:error,
|
{:error,
|
||||||
"Session metric `#{metric}` cannot be queried when using a filter on `#{event_only_filter}`."}
|
"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
|
||||||
end
|
end
|
||||||
|
|
||||||
defp find_event_only_filter(query) do
|
defp validate_metric(metric, _, _) do
|
||||||
Map.keys(query.filters) |> Enum.find(&event_only_property?/1)
|
{:error,
|
||||||
|
"The metric `#{metric}` is not recognized. Find valid metrics from the documentation: https://plausible.io/docs/stats-api#metrics"}
|
||||||
end
|
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
|
def timeseries(conn, params) do
|
||||||
site = Repo.preload(conn.assigns.site, :owner)
|
site = Repo.preload(conn.assigns.site, :owner)
|
||||||
|
|
||||||
@ -248,6 +206,19 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
|||||||
end
|
end
|
||||||
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
|
defp validate_date(%{"period" => "custom"} = params) do
|
||||||
with {:ok, date} <- Map.fetch(params, "date"),
|
with {:ok, date} <- Map.fetch(params, "date"),
|
||||||
[from, to] <- String.split(date, ","),
|
[from, to] <- String.split(date, ","),
|
||||||
|
@ -346,23 +346,61 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_top_stats(site, %Query{filters: %{"event:goal" => _}} = query, comparison_query) do
|
defp fetch_top_stats(site, %Query{filters: %{"event:goal" => _}} = query, comparison_query) do
|
||||||
metrics =
|
query_without_filters = Query.remove_event_filters(query, [:goal, :props])
|
||||||
[:total_visitors, :visitors, :events, :conversion_rate] ++ @revenue_metrics
|
metrics = [:visitors, :events] ++ @revenue_metrics
|
||||||
|
|
||||||
results = Stats.aggregate(site, query, metrics)
|
results_without_filters =
|
||||||
comparison = if comparison_query, do: Stats.aggregate(site, comparison_query, metrics)
|
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 visitors", :unique_visitors),
|
||||||
top_stats_entry(results, comparison, "Unique conversions", :visitors),
|
top_stats_entry(results, comparison, "Unique conversions", :converted_visitors),
|
||||||
top_stats_entry(results, comparison, "Total conversions", :events),
|
top_stats_entry(results, comparison, "Total conversions", :completions),
|
||||||
on_full_build do
|
on_full_build do
|
||||||
top_stats_entry(results, comparison, "Average revenue", :average_revenue, &format_money/1)
|
top_stats_entry(results, comparison, "Average revenue", :average_revenue, &format_money/1)
|
||||||
end,
|
end,
|
||||||
on_full_build do
|
on_full_build do
|
||||||
top_stats_entry(results, comparison, "Total revenue", :total_revenue, &format_money/1)
|
top_stats_entry(results, comparison, "Total revenue", :total_revenue, &format_money/1)
|
||||||
end,
|
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)
|
|> Enum.reject(&is_nil/1)
|
||||||
|> then(&{&1, 100})
|
|> then(&{&1, 100})
|
||||||
@ -431,16 +469,17 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
def sources(conn, params) do
|
def sources(conn, params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
|
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
|
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
|
|
||||||
extra_metrics =
|
metrics =
|
||||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
if params["detailed"], do: [:visitors, :bounce_rate, :visit_duration], else: [:visitors]
|
||||||
|
|
||||||
metrics = breakdown_metrics(query, extra_metrics)
|
|
||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:source", metrics, pagination)
|
Stats.breakdown(site, query, "visit:source", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :source, "visit:source")
|
||||||
|> transform_keys(%{source: :name})
|
|> transform_keys(%{source: :name})
|
||||||
|
|
||||||
if params["csv"] do
|
if params["csv"] do
|
||||||
@ -513,12 +552,16 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
def utm_mediums(conn, params) do
|
def utm_mediums(conn, params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
|
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
|
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
|
|
||||||
|
metrics = [:visitors, :bounce_rate, :visit_duration]
|
||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:utm_medium", metrics, pagination)
|
Stats.breakdown(site, query, "visit:utm_medium", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :utm_medium, "visit:utm_medium")
|
||||||
|> transform_keys(%{utm_medium: :name})
|
|> transform_keys(%{utm_medium: :name})
|
||||||
|
|
||||||
if params["csv"] do
|
if params["csv"] do
|
||||||
@ -536,12 +579,16 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
def utm_campaigns(conn, params) do
|
def utm_campaigns(conn, params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
|
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
|
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
|
|
||||||
|
metrics = [:visitors, :bounce_rate, :visit_duration]
|
||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:utm_campaign", metrics, pagination)
|
Stats.breakdown(site, query, "visit:utm_campaign", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :utm_campaign, "visit:utm_campaign")
|
||||||
|> transform_keys(%{utm_campaign: :name})
|
|> transform_keys(%{utm_campaign: :name})
|
||||||
|
|
||||||
if params["csv"] do
|
if params["csv"] do
|
||||||
@ -559,12 +606,15 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
def utm_contents(conn, params) do
|
def utm_contents(conn, params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
|
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
|
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
|
metrics = [:visitors, :bounce_rate, :visit_duration]
|
||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:utm_content", metrics, pagination)
|
Stats.breakdown(site, query, "visit:utm_content", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :utm_content, "visit:utm_content")
|
||||||
|> transform_keys(%{utm_content: :name})
|
|> transform_keys(%{utm_content: :name})
|
||||||
|
|
||||||
if params["csv"] do
|
if params["csv"] do
|
||||||
@ -582,12 +632,15 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
def utm_terms(conn, params) do
|
def utm_terms(conn, params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
|
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
|
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
|
metrics = [:visitors, :bounce_rate, :visit_duration]
|
||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:utm_term", metrics, pagination)
|
Stats.breakdown(site, query, "visit:utm_term", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :utm_term, "visit:utm_term")
|
||||||
|> transform_keys(%{utm_term: :name})
|
|> transform_keys(%{utm_term: :name})
|
||||||
|
|
||||||
if params["csv"] do
|
if params["csv"] do
|
||||||
@ -605,12 +658,16 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
def utm_sources(conn, params) do
|
def utm_sources(conn, params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
|
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
|
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
|
|
||||||
|
metrics = [:visitors, :bounce_rate, :visit_duration]
|
||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:utm_source", metrics, pagination)
|
Stats.breakdown(site, query, "visit:utm_source", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :utm_source, "visit:utm_source")
|
||||||
|> transform_keys(%{utm_source: :name})
|
|> transform_keys(%{utm_source: :name})
|
||||||
|
|
||||||
if params["csv"] do
|
if params["csv"] do
|
||||||
@ -628,12 +685,16 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
def referrers(conn, params) do
|
def referrers(conn, params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
|
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
|
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, [:bounce_rate, :visit_duration])
|
|
||||||
|
metrics = [:visitors, :bounce_rate, :visit_duration]
|
||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:referrer", metrics, pagination)
|
Stats.breakdown(site, query, "visit:referrer", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :referrer, "visit:referrer")
|
||||||
|> transform_keys(%{referrer: :name})
|
|> transform_keys(%{referrer: :name})
|
||||||
|
|
||||||
if params["csv"] do
|
if params["csv"] do
|
||||||
@ -693,13 +754,12 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
|
|
||||||
extra_metrics =
|
metrics =
|
||||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
if params["detailed"], do: [:visitors, :bounce_rate, :visit_duration], else: [:visitors]
|
||||||
|
|
||||||
metrics = breakdown_metrics(query, extra_metrics)
|
|
||||||
|
|
||||||
referrers =
|
referrers =
|
||||||
Stats.breakdown(site, query, "visit:referrer", metrics, pagination)
|
Stats.breakdown(site, query, "visit:referrer", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :referrer, "visit:referrer")
|
||||||
|> transform_keys(%{referrer: :name})
|
|> transform_keys(%{referrer: :name})
|
||||||
|
|
||||||
json(conn, referrers)
|
json(conn, referrers)
|
||||||
@ -709,16 +769,16 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
|
|
||||||
extra_metrics =
|
metrics =
|
||||||
if params["detailed"],
|
if params["detailed"],
|
||||||
do: [:pageviews, :bounce_rate, :time_on_page],
|
do: [:visitors, :pageviews, :bounce_rate, :time_on_page],
|
||||||
else: []
|
else: [:visitors]
|
||||||
|
|
||||||
metrics = breakdown_metrics(query, extra_metrics)
|
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
|
|
||||||
pages =
|
pages =
|
||||||
Stats.breakdown(site, query, "event:page", metrics, pagination)
|
Stats.breakdown(site, query, "event:page", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :page, "event:page")
|
||||||
|> transform_keys(%{page: :name})
|
|> transform_keys(%{page: :name})
|
||||||
|
|
||||||
if params["csv"] do
|
if params["csv"] do
|
||||||
@ -738,10 +798,11 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, [:visits, :visit_duration])
|
metrics = [:visitors, :visits, :visit_duration]
|
||||||
|
|
||||||
entry_pages =
|
entry_pages =
|
||||||
Stats.breakdown(site, query, "visit:entry_page", metrics, pagination)
|
Stats.breakdown(site, query, "visit:entry_page", metrics, pagination)
|
||||||
|
|> add_cr(site, query, pagination, :entry_page, "visit:entry_page")
|
||||||
|> transform_keys(%{entry_page: :name})
|
|> transform_keys(%{entry_page: :name})
|
||||||
|
|
||||||
if params["csv"] do
|
if params["csv"] do
|
||||||
@ -768,10 +829,11 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
{limit, page} = parse_pagination(params)
|
{limit, page} = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, [:visits])
|
metrics = [:visitors, :visits]
|
||||||
|
|
||||||
exit_pages =
|
exit_pages =
|
||||||
Stats.breakdown(site, query, "visit:exit_page", metrics, {limit, page})
|
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)
|
|> add_exit_rate(site, query, limit)
|
||||||
|> transform_keys(%{exit_page: :name})
|
|> transform_keys(%{exit_page: :name})
|
||||||
|
|
||||||
@ -827,10 +889,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = site |> Query.from(params)
|
query = site |> Query.from(params)
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query)
|
|
||||||
|
|
||||||
countries =
|
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})
|
|> transform_keys(%{country: :code})
|
||||||
|> add_percentages(site, query)
|
|> add_percentages(site, query)
|
||||||
|
|
||||||
@ -879,10 +941,9 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = site |> Query.from(params)
|
query = site |> Query.from(params)
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query)
|
|
||||||
|
|
||||||
regions =
|
regions =
|
||||||
Stats.breakdown(site, query, "visit:region", metrics, pagination)
|
Stats.breakdown(site, query, "visit:region", [:visitors], pagination)
|
||||||
|> transform_keys(%{region: :code})
|
|> transform_keys(%{region: :code})
|
||||||
|> Enum.map(fn region ->
|
|> Enum.map(fn region ->
|
||||||
region_entry = Location.get_subdivision(region[:code])
|
region_entry = Location.get_subdivision(region[:code])
|
||||||
@ -913,10 +974,9 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = site |> Query.from(params)
|
query = site |> Query.from(params)
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query)
|
|
||||||
|
|
||||||
cities =
|
cities =
|
||||||
Stats.breakdown(site, query, "visit:city", metrics, pagination)
|
Stats.breakdown(site, query, "visit:city", [:visitors], pagination)
|
||||||
|> transform_keys(%{city: :code})
|
|> transform_keys(%{city: :code})
|
||||||
|> Enum.map(fn city ->
|
|> Enum.map(fn city ->
|
||||||
city_info = Location.get_city(city[:code])
|
city_info = Location.get_city(city[:code])
|
||||||
@ -952,10 +1012,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query)
|
|
||||||
|
|
||||||
browsers =
|
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})
|
|> transform_keys(%{browser: :name})
|
||||||
|> add_percentages(site, query)
|
|> add_percentages(site, query)
|
||||||
|
|
||||||
@ -976,10 +1036,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query)
|
|
||||||
|
|
||||||
versions =
|
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})
|
|> transform_keys(%{browser_version: :name})
|
||||||
|> add_percentages(site, query)
|
|> add_percentages(site, query)
|
||||||
|
|
||||||
@ -1006,10 +1066,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query)
|
|
||||||
|
|
||||||
systems =
|
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})
|
|> transform_keys(%{os: :name})
|
||||||
|> add_percentages(site, query)
|
|> add_percentages(site, query)
|
||||||
|
|
||||||
@ -1030,10 +1090,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query)
|
|
||||||
|
|
||||||
versions =
|
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})
|
|> transform_keys(%{os_version: :name})
|
||||||
|> add_percentages(site, query)
|
|> add_percentages(site, query)
|
||||||
|
|
||||||
@ -1044,10 +1104,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site, params)
|
query = Query.from(site, params)
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query)
|
|
||||||
|
|
||||||
sizes =
|
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})
|
|> transform_keys(%{device: :name})
|
||||||
|> add_percentages(site, query)
|
|> add_percentages(site, query)
|
||||||
|
|
||||||
@ -1064,6 +1124,14 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
end
|
end
|
||||||
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
|
def conversions(conn, params) do
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
site = Plausible.Repo.preload(conn.assigns.site, :goals)
|
site = Plausible.Repo.preload(conn.assigns.site, :goals)
|
||||||
@ -1076,7 +1144,21 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
query
|
query
|
||||||
end
|
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 =
|
conversions =
|
||||||
site
|
site
|
||||||
@ -1084,6 +1166,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|> transform_keys(%{goal: :name})
|
|> transform_keys(%{goal: :name})
|
||||||
|> Enum.map(fn goal ->
|
|> Enum.map(fn goal ->
|
||||||
goal
|
goal
|
||||||
|
|> Map.put(:conversion_rate, calculate_cr(total_visitors, goal[:visitors]))
|
||||||
|> Enum.map(&format_revenue_metric/1)
|
|> Enum.map(&format_revenue_metric/1)
|
||||||
|> Map.new()
|
|> Map.new()
|
||||||
end)
|
end)
|
||||||
@ -1157,19 +1240,32 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|> Map.put(:include_imported, false)
|
|> Map.put(:include_imported, false)
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
if query.filters["event:goal"] do
|
if full_build?() and Map.has_key?(query.filters, "event:goal") do
|
||||||
[:visitors, :events, :conversion_rate] ++ @revenue_metrics
|
|
||||||
else
|
|
||||||
[:visitors, :events] ++ @revenue_metrics
|
[:visitors, :events] ++ @revenue_metrics
|
||||||
|
else
|
||||||
|
[:visitors, :events]
|
||||||
end
|
end
|
||||||
|
|
||||||
Stats.breakdown(site, query, prefixed_prop, metrics, pagination)
|
props =
|
||||||
|> transform_keys(%{prop_key => :name})
|
Stats.breakdown(site, query, prefixed_prop, metrics, pagination)
|
||||||
|> Enum.map(fn entry ->
|
|> transform_keys(%{prop_key => :name})
|
||||||
Enum.map(entry, &format_revenue_metric/1)
|
|> Enum.map(fn entry ->
|
||||||
|> Map.new()
|
Enum.map(entry, &format_revenue_metric/1)
|
||||||
end)
|
|> Map.new()
|
||||||
|> add_percentages(site, query)
|
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
|
end
|
||||||
|
|
||||||
def current_visitors(conn, _) do
|
def current_visitors(conn, _) do
|
||||||
@ -1228,6 +1324,37 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
defp add_percentages(breakdown_result, _, _), do: breakdown_result
|
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), do: to_csv(list, columns, columns)
|
||||||
|
|
||||||
defp to_csv(list, columns, column_names) do
|
defp to_csv(list, columns, column_names) do
|
||||||
@ -1358,12 +1485,4 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
{metric, value}
|
{metric, value}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
@ -140,22 +140,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
|||||||
end
|
end
|
||||||
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", %{
|
test "validates that views_per_visit cannot be used with event:page filter", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
site: site
|
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`."
|
"Metric `views_per_visit` cannot be queried with a filter on `event:page`."
|
||||||
}
|
}
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
test "aggregates a single metric", %{conn: conn, site: site} do
|
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"] == %{
|
assert json_response(conn, 200)["results"] == %{
|
||||||
"pageviews" => %{"value" => 4, "change" => 100},
|
"pageviews" => %{"value" => 4, "change" => 100},
|
||||||
"visitors" => %{"value" => 3, "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}
|
"visit_duration" => %{"value" => 0, "change" => 0}
|
||||||
}
|
}
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "with imported data" do
|
describe "with imported data" do
|
||||||
@ -419,7 +357,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
|||||||
"visitors" => %{"value" => 2, "change" => 100},
|
"visitors" => %{"value" => 2, "change" => 100},
|
||||||
"visits" => %{"value" => 5, "change" => 150},
|
"visits" => %{"value" => 5, "change" => 150},
|
||||||
"pageviews" => %{"value" => 9, "change" => -10},
|
"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},
|
"views_per_visit" => %{"value" => 1.0, "change" => 100},
|
||||||
"visit_duration" => %{"value" => 20, "change" => -80}
|
"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}}
|
assert json_response(conn, 200)["results"] == %{"pageviews" => %{"value" => 3}}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
@ -83,23 +83,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "param validation" do
|
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
|
test "validates that property is required", %{conn: conn, site: site} do
|
||||||
conn =
|
conn =
|
||||||
get(conn, "/api/v1/stats/breakdown", %{
|
get(conn, "/api/v1/stats/breakdown", %{
|
||||||
@ -2018,308 +2001,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "metrics" do
|
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
|
test "all metrics for breakdown by visit prop", %{conn: conn, site: site} do
|
||||||
populate_stats(site, [
|
populate_stats(site, [
|
||||||
build(:pageview,
|
build(:pageview,
|
||||||
|
@ -1093,22 +1093,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.TimeseriesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "metrics" do
|
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
|
test "shows pageviews,visits,views_per_visit for last 7d", %{conn: conn, site: site} do
|
||||||
populate_stats(site, [
|
populate_stats(site, [
|
||||||
build(:pageview,
|
build(:pageview,
|
||||||
|
@ -1313,13 +1313,17 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
|||||||
%{
|
%{
|
||||||
"total_visitors" => 2,
|
"total_visitors" => 2,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
|
"visits" => 1,
|
||||||
"name" => "/page1",
|
"name" => "/page1",
|
||||||
|
"visit_duration" => 0,
|
||||||
"conversion_rate" => 50.0
|
"conversion_rate" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"total_visitors" => 1,
|
"total_visitors" => 1,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
|
"visits" => 1,
|
||||||
"name" => "/page2",
|
"name" => "/page2",
|
||||||
|
"visit_duration" => 900,
|
||||||
"conversion_rate" => 100.0
|
"conversion_rate" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1504,12 +1508,14 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
|||||||
"name" => "/exit1",
|
"name" => "/exit1",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"total_visitors" => 1,
|
"total_visitors" => 1,
|
||||||
|
"visits" => 1,
|
||||||
"conversion_rate" => 100.0
|
"conversion_rate" => 100.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/exit2",
|
"name" => "/exit2",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"total_visitors" => 1,
|
"total_visitors" => 1,
|
||||||
|
"visits" => 1,
|
||||||
"conversion_rate" => 100.0
|
"conversion_rate" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1094,7 +1094,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
|
|||||||
res = json_response(conn, 200)
|
res = json_response(conn, 200)
|
||||||
|
|
||||||
assert %{
|
assert %{
|
||||||
"change" => -33.4,
|
"change" => -50,
|
||||||
"comparison_value" => 66.7,
|
"comparison_value" => 66.7,
|
||||||
"name" => "Conversion rate",
|
"name" => "Conversion rate",
|
||||||
"value" => 33.3
|
"value" => 33.3
|
||||||
|
Loading…
Reference in New Issue
Block a user