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:
Vini Brasil 2023-07-15 12:04:29 +01:00 committed by GitHub
parent 626dcba0c5
commit 35107e2b8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 96 additions and 29 deletions

View File

@ -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>
)
}

View 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 "-"
}
}

View File

@ -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>
)

View File

@ -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

View File

@ -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

View File

@ -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: "/"),