mirror of
https://github.com/plausible/analytics.git
synced 2024-09-11 18:07:33 +03:00
Add revenue metrics to prop breakdown (#3140)
* Extract <Money> React component * Unhide prop breakdown for revenue goals * Query revenue metrics on prop breakdown API
This commit is contained in:
parent
626dcba0c5
commit
35107e2b8f
@ -9,14 +9,7 @@ import * as api from '../../api'
|
||||
import * as url from '../../util/url'
|
||||
import { escapeFilterValue } from '../../util/filters'
|
||||
import LazyLoader from '../../components/lazy-loader'
|
||||
|
||||
function Money({ formatted }) {
|
||||
if (formatted) {
|
||||
return <span tooltip={formatted.long}>{formatted.short}</span>
|
||||
} else {
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
import Money from './money'
|
||||
|
||||
export default class Conversions extends React.Component {
|
||||
constructor(props) {
|
||||
@ -75,7 +68,7 @@ export default class Conversions extends React.Component {
|
||||
{renderRevenueColumn && <span className="hidden md:inline-block md:w-20 font-medium text-right"><Money formatted={goal.average_revenue} /></span>}
|
||||
</div>
|
||||
</div>
|
||||
{ renderProps && !goal.total_revenue && <PropBreakdown site={this.props.site} query={this.props.query} goal={goal} /> }
|
||||
{ renderProps && <PropBreakdown site={this.props.site} query={this.props.query} goal={goal} renderRevenueColumn={renderRevenueColumn } /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
10
assets/js/dashboard/stats/behaviours/money.js
Normal file
10
assets/js/dashboard/stats/behaviours/money.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Money({ formatted }) {
|
||||
if (formatted) {
|
||||
return <span tooltip={formatted.long}>{formatted.short}</span>
|
||||
} else {
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import * as storage from '../../util/storage'
|
||||
import Bar from '../bar'
|
||||
import numberFormatter from '../../util/number-formatter'
|
||||
import * as api from '../../api'
|
||||
import Money from './money'
|
||||
|
||||
const MOBILE_UPPER_WIDTH = 767
|
||||
const DEFAULT_WIDTH = 1080
|
||||
@ -71,11 +72,6 @@ export default class PropertyBreakdown extends React.Component {
|
||||
this.setState({ viewport: window.innerWidth });
|
||||
}
|
||||
|
||||
getBarMaxWidth() {
|
||||
const { viewport } = this.state;
|
||||
return viewport > MOBILE_UPPER_WIDTH ? "16rem" : "10rem";
|
||||
}
|
||||
|
||||
fetch({concat}) {
|
||||
if (!this.props.query.filters['goal']) return
|
||||
|
||||
@ -135,15 +131,16 @@ export default class PropertyBreakdown extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-2" key={value.name}>
|
||||
<Bar
|
||||
count={value.unique_conversions}
|
||||
plot="unique_conversions"
|
||||
all={this.state.breakdown}
|
||||
bg="bg-red-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction={this.getBarMaxWidth()}
|
||||
>
|
||||
{this.renderPropContent(value, query)}
|
||||
</Bar>
|
||||
<div className="flex-1">
|
||||
<Bar
|
||||
count={value.unique_conversions}
|
||||
plot="unique_conversions"
|
||||
all={this.state.breakdown}
|
||||
bg="bg-red-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
>
|
||||
{this.renderPropContent(value, query)}
|
||||
</Bar>
|
||||
</div>
|
||||
<div className="dark:text-gray-200">
|
||||
<span className="font-medium inline-block w-20 text-right">{numberFormatter(value.unique_conversions)}</span>
|
||||
{
|
||||
@ -157,6 +154,8 @@ export default class PropertyBreakdown extends React.Component {
|
||||
: null
|
||||
}
|
||||
<span className="font-medium inline-block w-20 text-right">{numberFormatter(value.conversion_rate)}%</span>
|
||||
{this.props.renderRevenueColumn && <span className="hidden md:inline-block md:w-20 font-medium text-right"><Money formatted={value.total_revenue} /></span>}
|
||||
{this.props.renderRevenueColumn && <span className="hidden md:inline-block md:w-20 font-medium text-right"><Money formatted={value.average_revenue} /></span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -69,6 +69,7 @@ defmodule Plausible.Stats.Breakdown do
|
||||
|
||||
def breakdown(site, query, "event:props:" <> custom_prop = property, metrics, pagination) do
|
||||
{limit, _} = pagination
|
||||
{currency, metrics} = get_revenue_tracking_currency(site, query, metrics)
|
||||
|
||||
none_result =
|
||||
if include_none_result?(query.filters[property]) do
|
||||
@ -90,6 +91,7 @@ defmodule Plausible.Stats.Breakdown do
|
||||
results =
|
||||
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)], :desc)
|
||||
|
||||
if Enum.find_index(results, fn value -> value[custom_prop] == "(none)" end) == limit do
|
||||
|
@ -1178,18 +1178,23 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
prop_name = "event:props:" <> params["prop_name"]
|
||||
|
||||
props =
|
||||
Stats.breakdown(site, query, prop_name, [:visitors, :events], pagination)
|
||||
Stats.breakdown(
|
||||
site,
|
||||
query,
|
||||
prop_name,
|
||||
[:visitors, :events, :average_revenue, :total_revenue],
|
||||
pagination
|
||||
)
|
||||
|> transform_keys(%{
|
||||
params["prop_name"] => :name,
|
||||
:events => :total_conversions,
|
||||
:visitors => :unique_conversions
|
||||
})
|
||||
|> Enum.map(fn prop ->
|
||||
Map.put(
|
||||
prop,
|
||||
:conversion_rate,
|
||||
calculate_cr(unique_visitors, prop[:unique_conversions])
|
||||
)
|
||||
prop
|
||||
|> Map.put(:conversion_rate, calculate_cr(unique_visitors, prop[:unique_conversions]))
|
||||
|> Enum.map(&format_revenue_metric/1)
|
||||
|> Map.new()
|
||||
end)
|
||||
|
||||
if params["csv"] do
|
||||
|
@ -852,6 +852,64 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "returns property breakdown for revenue goal", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview, pathname: "/"),
|
||||
build(:pageview, pathname: "/"),
|
||||
build(:pageview, pathname: "/register"),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
revenue_reporting_amount: Decimal.new("132.21"),
|
||||
revenue_reporting_currency: "EUR",
|
||||
"meta.key": ["method"],
|
||||
"meta.value": ["card"]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
revenue_reporting_amount: Decimal.new("412.30"),
|
||||
revenue_reporting_currency: "EUR",
|
||||
"meta.key": ["method"],
|
||||
"meta.value": ["cash"]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
revenue_reporting_amount: Decimal.new("30.23"),
|
||||
revenue_reporting_currency: "EUR",
|
||||
"meta.key": ["method"],
|
||||
"meta.value": ["cash"]
|
||||
)
|
||||
])
|
||||
|
||||
insert(:goal, site: site, event_name: "Purchase", currency: "EUR")
|
||||
filters = Jason.encode!(%{goal: "Purchase"})
|
||||
prop_key = "method"
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"average_revenue" => %{"long" => "€221.26", "short" => "€221.3"},
|
||||
"conversion_rate" => 33.3,
|
||||
"name" => "cash",
|
||||
"total_conversions" => 2,
|
||||
"total_revenue" => %{"long" => "€442.53", "short" => "€442.5"},
|
||||
"unique_conversions" => 2
|
||||
},
|
||||
%{
|
||||
"average_revenue" => %{"long" => "€132.21", "short" => "€132.2"},
|
||||
"conversion_rate" => 16.7,
|
||||
"name" => "card",
|
||||
"total_conversions" => 1,
|
||||
"total_revenue" => %{"long" => "€132.21", "short" => "€132.2"},
|
||||
"unique_conversions" => 1
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns (none) values in property breakdown for goal", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview, pathname: "/"),
|
||||
|
Loading…
Reference in New Issue
Block a user