mirror of
https://github.com/plausible/analytics.git
synced 2024-12-22 17:11:36 +03:00
Revenue tracking: goal conversions and tracker script (#3019)
* Add revenue average and total to Goal Conversions * Add revenue option to tracker script * Simplify revenue tracker script
This commit is contained in:
parent
fd15853965
commit
8f6224b5de
@ -10,36 +10,27 @@ import * as url from '../../util/url'
|
||||
import { escapeFilterValue } from '../../util/filters'
|
||||
import LazyLoader from '../../components/lazy-loader'
|
||||
|
||||
const MOBILE_UPPER_WIDTH = 767
|
||||
const DEFAULT_WIDTH = 1080
|
||||
function Money({ formatted }) {
|
||||
if (formatted) {
|
||||
return <span tooltip={formatted.long}>{formatted.short}</span>
|
||||
} else {
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
|
||||
export default class Conversions extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.htmlNode = React.createRef()
|
||||
this.state = {
|
||||
loading: true,
|
||||
viewport: DEFAULT_WIDTH,
|
||||
}
|
||||
this.state = { loading: true, }
|
||||
this.onVisible = this.onVisible.bind(this)
|
||||
this.fetchConversions = this.fetchConversions.bind(this)
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize, false);
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize, false);
|
||||
document.removeEventListener('tick', this.fetchConversions)
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.setState({ viewport: window.innerWidth });
|
||||
}
|
||||
|
||||
onVisible() {
|
||||
this.fetchConversions()
|
||||
if (this.props.query.period === 'realtime') {
|
||||
@ -55,60 +46,60 @@ export default class Conversions extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getBarMaxWidth() {
|
||||
const { viewport } = this.state;
|
||||
return viewport > MOBILE_UPPER_WIDTH ? "16rem" : "10rem";
|
||||
}
|
||||
|
||||
fetchConversions() {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/conversions`, this.props.query)
|
||||
.then((res) => this.setState({ loading: false, goals: res, prevHeight: null }))
|
||||
}
|
||||
|
||||
renderGoal(goal) {
|
||||
const { viewport } = this.state;
|
||||
renderGoal(goal, renderRevenueColumn) {
|
||||
const renderProps = this.props.query.filters['goal'] == goal.name && goal.prop_names
|
||||
|
||||
return (
|
||||
<div className="my-2 text-sm" key={goal.name}>
|
||||
<div className="flex items-center justify-between my-2">
|
||||
<span className="flex-1">
|
||||
<Bar
|
||||
count={goal.unique_conversions}
|
||||
all={this.state.goals}
|
||||
bg="bg-red-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction={this.getBarMaxWidth()}
|
||||
plot="unique_conversions"
|
||||
>
|
||||
<Link to={url.setQuery('goal', escapeFilterValue(goal.name))} className="block px-2 py-1.5 hover:underline relative z-9 break-all lg:truncate dark:text-gray-200">{goal.name}</Link>
|
||||
</Bar>
|
||||
</span>
|
||||
<div className="dark:text-gray-200">
|
||||
<span className="inline-block w-20 font-medium text-right">{numberFormatter(goal.unique_conversions)}</span>
|
||||
{viewport > MOBILE_UPPER_WIDTH && <span className="inline-block w-20 font-medium text-right">{numberFormatter(goal.total_conversions)}</span>}
|
||||
<span className="hidden md:inline-block md:w-20 font-medium text-right">{numberFormatter(goal.total_conversions)}</span>
|
||||
<span className="inline-block w-20 font-medium text-right">{goal.conversion_rate}%</span>
|
||||
{renderRevenueColumn && <span className="hidden md:inline-block md:w-20 font-medium text-right"><Money formatted={goal.total_revenue} /></span>}
|
||||
{renderRevenueColumn && <span className="hidden md:inline-block md:w-20 font-medium text-right"><Money formatted={goal.average_revenue} /></span>}
|
||||
</div>
|
||||
</div>
|
||||
{renderProps && <PropBreakdown site={this.props.site} query={this.props.query} goal={goal} />}
|
||||
{ renderProps && !goal.total_revenue && <PropBreakdown site={this.props.site} query={this.props.query} goal={goal} /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderInner() {
|
||||
const { viewport } = this.state;
|
||||
if (this.state.loading) {
|
||||
return <div className="mx-auto my-2 loading"><div></div></div>
|
||||
} else if (this.state.goals) {
|
||||
const hasRevenue = this.state.goals.some((goal) => goal.total_revenue)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<span>Goal</span>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">Uniques</span>
|
||||
{viewport > MOBILE_UPPER_WIDTH && <span className="inline-block w-20">Total</span>}
|
||||
<span className="hidden md:inline-block md:w-20">Total</span>
|
||||
<span className="inline-block w-20">CR</span>
|
||||
{hasRevenue && <span className="hidden md:inline-block md:w-20">Revenue</span>}
|
||||
{hasRevenue && <span className="hidden md:inline-block md:w-20">Average</span>}
|
||||
</div>
|
||||
</div>
|
||||
<FlipMove>
|
||||
{this.state.goals.map(this.renderGoal.bind(this))}
|
||||
{ this.state.goals.map((goal) => this.renderGoal.bind(this)(goal, hasRevenue) ) }
|
||||
</FlipMove>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
@ -11,7 +11,8 @@ defmodule PlausibleWeb.Tracker do
|
||||
"manual",
|
||||
"file-downloads",
|
||||
"pageview-props",
|
||||
"tagged-events"
|
||||
"tagged-events",
|
||||
"revenue"
|
||||
]
|
||||
|
||||
# Generates Power Set of all variants
|
||||
|
@ -26,7 +26,7 @@ function compilefile(input, output, templateVars = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const base_variants = ["hash", "outbound-links", "exclusions", "compat", "local", "manual", "file-downloads", "pageview-props", "tagged-events"]
|
||||
const base_variants = ["hash", "outbound-links", "exclusions", "compat", "local", "manual", "file-downloads", "pageview-props", "tagged-events", "revenue"]
|
||||
const variants = [...g.clone.powerSet(base_variants)].filter(a => a.length > 0).map(a => a.sort());
|
||||
|
||||
compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.js'))
|
||||
|
@ -81,6 +81,11 @@
|
||||
if (options && options.props) {
|
||||
payload.p = options.props
|
||||
}
|
||||
{{#if revenue}}
|
||||
if (options && options.revenue) {
|
||||
payload.$ = options.revenue
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
{{#if pageview_props}}
|
||||
var propAttributes = scriptEl.getAttributeNames().filter(function (name) {
|
||||
|
18
tracker/test/fixtures/revenue.html
vendored
Normal file
18
tracker/test/fixtures/revenue.html
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Plausible Playwright tests</title>
|
||||
|
||||
<script defer src="/tracker/js/plausible.local.manual.revenue.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button id="purchase" onclick="window.plausible('Purchase', {revenue: {amount: 15.99, currency: 'USD'}})">
|
||||
Click me
|
||||
</button>
|
||||
</body>
|
||||
</html>
|
13
tracker/test/revenue.spec.js
Normal file
13
tracker/test/revenue.spec.js
Normal file
@ -0,0 +1,13 @@
|
||||
const { mockRequest, expectCustomEvent } = require('./support/test-utils');
|
||||
const { expect, test } = require('@playwright/test');
|
||||
|
||||
test.describe('with revenue script extension', () => {
|
||||
test('sends revenue currency and amount', async ({ page }) => {
|
||||
const plausibleRequestMock = mockRequest(page, '/api/event')
|
||||
await page.goto('/revenue.html');
|
||||
await page.click('#purchase')
|
||||
|
||||
const plausibleRequest = await plausibleRequestMock
|
||||
expect(plausibleRequest.postDataJSON()["$"]).toEqual({amount: 15.99, currency: "USD"})
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user