mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 09:33:19 +03:00
Add conversion_rate to sources api and source table (#1299)
* Add conversion_rate to sources api and source table * Remove percentageFormatter * Update source tests to include conversionat rate * Add CR to detals modal * Correct formatting with linter * Add change log * Add CR to Pages, Device and Countries panels
This commit is contained in:
parent
d9a6ac4fd1
commit
b3bc796d50
@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Added `CLICKHOUSE_FLUSH_INTERVAL_MS` and `CLICKHOUSE_MAX_BUFFER_SIZE` configuration parameters plausible/analytics#1073
|
||||
- Ability to invite users to sites with different roles plausible/analytics#1122
|
||||
- Option to configure a custom name for the script file
|
||||
- Add Conversion Rate to Top Sources, Top Pages Devices, Countries when filtered by a goal plausible/analytics#1299
|
||||
|
||||
### Fixed
|
||||
- Fix weekly report time range plausible/analytics#951
|
||||
|
@ -21,7 +21,7 @@ export default class Browsers extends React.Component {
|
||||
this.fetchBrowsers()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onVisible() {
|
||||
this.fetchBrowsers()
|
||||
if (this.props.timer) this.props.timer.onTick(this.fetchBrowsers.bind(this))
|
||||
@ -37,6 +37,10 @@ export default class Browsers extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
label() {
|
||||
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||
}
|
||||
@ -58,6 +62,7 @@ export default class Browsers extends React.Component {
|
||||
} else {
|
||||
query.set('browser', browser.name)
|
||||
}
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={browser.name}>
|
||||
@ -65,14 +70,14 @@ export default class Browsers extends React.Component {
|
||||
count={browser.count}
|
||||
all={this.state.browsers}
|
||||
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction="6rem"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
{this.renderBrowserContent(browser, query)}
|
||||
</Bar>
|
||||
<span className="font-medium dark:text-gray-200">
|
||||
{numberFormatter(browser.count)}
|
||||
<span className="inline-block w-8 text-xs text-right">({browser.percentage}%)</span>
|
||||
<span className="font-medium dark:text-gray-200 text-right w-20">
|
||||
{numberFormatter(browser.count)} <span className="inline-block w-8 text-xs"> ({browser.percentage}%)</span>
|
||||
</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(browser.conversion_rate)}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -85,7 +90,10 @@ export default class Browsers extends React.Component {
|
||||
<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>{ key }</span>
|
||||
<span>{ this.label() }</span>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">{ this.label() }</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</div>
|
||||
{ this.state.browsers && this.state.browsers.map(this.renderBrowser.bind(this)) }
|
||||
</React.Fragment>
|
||||
|
@ -57,7 +57,6 @@ class ScreenSizes extends React.Component {
|
||||
if (this.props.timer) this.props.timer.onTick(this.fetchScreenSizes.bind(this))
|
||||
}
|
||||
|
||||
|
||||
fetchScreenSizes() {
|
||||
api.get(
|
||||
`/api/stats/${encodeURIComponent(this.props.site.domain)}/screen-sizes`,
|
||||
@ -66,6 +65,11 @@ class ScreenSizes extends React.Component {
|
||||
.then((res) => this.setState({loading: false, sizes: res}))
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
|
||||
label() {
|
||||
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||
}
|
||||
@ -73,6 +77,7 @@ class ScreenSizes extends React.Component {
|
||||
renderScreenSize(size) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('screen', size.name)
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={size.name}>
|
||||
@ -80,7 +85,7 @@ class ScreenSizes extends React.Component {
|
||||
count={size.count}
|
||||
all={this.state.sizes}
|
||||
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction="6rem"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
<span
|
||||
tooltip={EXPLANATION[size.name]}
|
||||
@ -91,12 +96,10 @@ class ScreenSizes extends React.Component {
|
||||
</Link>
|
||||
</span>
|
||||
</Bar>
|
||||
<span
|
||||
className="font-medium dark:text-gray-200"
|
||||
>
|
||||
{numberFormatter(size.count)}
|
||||
<span className="inline-block w-8 text-xs text-right">({size.percentage}%)</span>
|
||||
<span className="font-medium dark:text-gray-200 text-right w-20">
|
||||
{numberFormatter(size.count)} <span className="inline-block w-8 text-xs text-right">({size.percentage}%)</span>
|
||||
</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(size.conversion_rate)}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -109,7 +112,10 @@ class ScreenSizes extends React.Component {
|
||||
className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500"
|
||||
>
|
||||
<span>Screen size</span>
|
||||
<span>{ this.label() }</span>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">{ this.label() }</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</div>
|
||||
{ this.state.sizes && this.state.sizes.map(this.renderScreenSize.bind(this)) }
|
||||
</React.Fragment>
|
||||
@ -146,14 +152,14 @@ export default class Devices extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
setMode(mode) {
|
||||
return () => {
|
||||
storage.setItem(this.tabKey, mode)
|
||||
this.setState({mode})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
renderContent() {
|
||||
switch (this.state.mode) {
|
||||
case 'browser':
|
||||
@ -190,7 +196,7 @@ export default class Devices extends React.Component {
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<li
|
||||
className="cursor-pointer hover:text-indigo-600"
|
||||
|
@ -36,6 +36,10 @@ export default class OperatingSystems extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
renderOperatingSystem(os) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
if (this.props.query.filters.os) {
|
||||
@ -43,6 +47,7 @@ export default class OperatingSystems extends React.Component {
|
||||
} else {
|
||||
query.set('os', os.name)
|
||||
}
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -53,7 +58,7 @@ export default class OperatingSystems extends React.Component {
|
||||
count={os.count}
|
||||
all={this.state.operatingSystems}
|
||||
bg="bg-green-50 dark:gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction="6rem"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
<span className="flex px-2 py-1.5 dark:text-gray-300 relative z-9 break-all">
|
||||
<Link className="md:truncate block hover:underline" to={{search: query.toString()}}>
|
||||
@ -61,7 +66,8 @@ export default class OperatingSystems extends React.Component {
|
||||
</Link>
|
||||
</span>
|
||||
</Bar>
|
||||
<span className="font-medium dark:text-gray-200">{numberFormatter(os.count)} <span className="inline-block w-8 text-xs text-right">({os.percentage}%)</span></span>
|
||||
<span className="font-medium dark:text-gray-200 text-right w-20">{numberFormatter(os.count)} <span className="inline-block w-8 text-xs text-right">({os.percentage}%)</span></span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(os.conversion_rate)}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -78,7 +84,10 @@ export default class OperatingSystems extends React.Component {
|
||||
<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>{ key }</span>
|
||||
<span>{ this.label() }</span>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">{ this.label() }</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</div>
|
||||
{ this.state.operatingSystems && this.state.operatingSystems.map(this.renderOperatingSystem.bind(this)) }
|
||||
</React.Fragment>
|
||||
|
@ -25,6 +25,10 @@ class CountriesModal extends React.Component {
|
||||
return this.state.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.state.query.filters.goal
|
||||
}
|
||||
|
||||
renderCountry(country) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('country', country.name)
|
||||
@ -47,6 +51,7 @@ class CountriesModal extends React.Component {
|
||||
<td className="p-2 w-32 font-medium" align="right">
|
||||
{numberFormatter(country.count)} <span className="inline-block text-xs w-8 text-right">({country.percentage}%)</span>
|
||||
</td>
|
||||
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{country.conversion_rate}%</td> }
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@ -57,7 +62,7 @@ class CountriesModal extends React.Component {
|
||||
<div className="loading mt-32 mx-auto"><div></div></div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (this.state.countries) {
|
||||
return (
|
||||
<>
|
||||
@ -81,6 +86,7 @@ class CountriesModal extends React.Component {
|
||||
>
|
||||
{this.label()}
|
||||
</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>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -56,6 +56,10 @@ class EntryPagesModal extends React.Component {
|
||||
return '-';
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.state.query.filters.goal
|
||||
}
|
||||
|
||||
renderPage(page) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('entry_page', page.name)
|
||||
@ -76,6 +80,7 @@ class EntryPagesModal extends React.Component {
|
||||
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.count)}</td>
|
||||
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.entries)}</td>
|
||||
{this.showVisitDuration() && <td className="p-2 w-32 font-medium" align="right">{durationFormatter(page.visit_duration)}</td>}
|
||||
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.conversion_rate)}%</td>}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@ -125,6 +130,11 @@ class EntryPagesModal extends React.Component {
|
||||
align="right"
|
||||
>Visit Duration
|
||||
</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>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -42,6 +42,10 @@ class ExitPagesModal extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.state.query.filters.goal
|
||||
}
|
||||
|
||||
renderPage(page) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('exit_page', page.name)
|
||||
@ -54,6 +58,7 @@ class ExitPagesModal extends React.Component {
|
||||
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.count)}</td>
|
||||
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.exits)}</td>
|
||||
<td className="p-2 w-32 font-medium" align="right">{this.formatPercentage(page.exit_rate)}</td>
|
||||
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.conversion_rate)}%</td>}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@ -87,6 +92,7 @@ class ExitPagesModal extends React.Component {
|
||||
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Unique Exits</th>
|
||||
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Total Exits</th>
|
||||
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Exit Rate</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>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -44,6 +44,10 @@ class PagesModal extends React.Component {
|
||||
return this.state.query.period !== 'realtime' && !(filters.goal || filters.source || filters.referrer)
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.state.query.filters.goal
|
||||
}
|
||||
|
||||
formatBounceRate(page) {
|
||||
if (typeof(page.bounce_rate) === 'number') {
|
||||
return page.bounce_rate + '%'
|
||||
@ -66,6 +70,7 @@ class PagesModal extends React.Component {
|
||||
{this.showPageviews() && <td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.pageviews)}</td> }
|
||||
{this.showExtra() && <td className="p-2 w-32 font-medium" align="right">{this.formatBounceRate(page)}</td> }
|
||||
{this.showExtra() && <td className="p-2 w-32 font-medium" align="right">{timeOnPage}</td> }
|
||||
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{page.conversion_rate}%</td> }
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@ -104,6 +109,7 @@ class PagesModal extends React.Component {
|
||||
{this.showPageviews() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Pageviews</th>}
|
||||
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Bounce rate</th>}
|
||||
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Time on Page</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>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -54,6 +54,10 @@ class SourcesModal extends React.Component {
|
||||
return this.state.query.period !== 'realtime' && !this.state.query.filters.goal
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.state.query.filters.goal
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.setState({loading: true, page: this.state.page + 1}, this.loadSources.bind(this))
|
||||
}
|
||||
@ -82,6 +86,8 @@ class SourcesModal extends React.Component {
|
||||
if (filter === 'utm_sources') query.set('utm_source', source.name)
|
||||
if (filter === 'utm_campaigns') query.set('utm_campaign', source.name)
|
||||
|
||||
console.log(source)
|
||||
|
||||
return (
|
||||
<tr className="text-sm dark:text-gray-200" key={source.name}>
|
||||
<td className="p-2">
|
||||
@ -94,6 +100,7 @@ class SourcesModal extends React.Component {
|
||||
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(source.count)}</td>
|
||||
{this.showExtra() && <td className="p-2 w-32 font-medium" align="right">{this.formatBounceRate(source)}</td> }
|
||||
{this.showExtra() && <td className="p-2 w-32 font-medium" align="right">{this.formatDuration(source)}</td> }
|
||||
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{source.conversion_rate}%</td> }
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@ -135,6 +142,7 @@ class SourcesModal extends React.Component {
|
||||
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">{this.label()}</th>
|
||||
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Bounce rate</th>}
|
||||
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Visit duration</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>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -29,6 +29,10 @@ export default class EntryPages extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
fetchPages() {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/entry-pages`, this.props.query)
|
||||
.then((res) => this.setState({loading: false, pages: res}))
|
||||
@ -37,6 +41,7 @@ export default class EntryPages extends React.Component {
|
||||
renderPage(page) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('entry_page', page.name)
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={page.name}>
|
||||
@ -44,7 +49,7 @@ export default class EntryPages extends React.Component {
|
||||
count={page.count}
|
||||
all={this.state.pages}
|
||||
bg="bg-orange-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction="4rem"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
<span className="flex px-2 py-1.5 group dark:text-gray-300 relative break-all z-9">
|
||||
<Link
|
||||
@ -62,7 +67,8 @@ export default class EntryPages extends React.Component {
|
||||
</a>
|
||||
</span>
|
||||
</Bar>
|
||||
<span className="font-medium dark:text-gray-200">{numberFormatter(page.count)}</span>
|
||||
<span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(page.count)}</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(page.conversion_rate)}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -73,7 +79,10 @@ export default class EntryPages extends React.Component {
|
||||
<>
|
||||
<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>Page url</span>
|
||||
<span>Unique Entrances</span>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-30">Unique Entrances</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlipMove>
|
||||
@ -81,7 +90,7 @@ export default class EntryPages extends React.Component {
|
||||
</FlipMove>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400"
|
||||
|
@ -29,6 +29,10 @@ export default class ExitPages extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
fetchPages() {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/exit-pages`, this.props.query)
|
||||
.then((res) => this.setState({loading: false, pages: res}))
|
||||
@ -37,6 +41,7 @@ export default class ExitPages extends React.Component {
|
||||
renderPage(page) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('exit_page', page.name)
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={page.name}>
|
||||
@ -44,7 +49,7 @@ export default class ExitPages extends React.Component {
|
||||
count={page.count}
|
||||
all={this.state.pages}
|
||||
bg="bg-orange-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction="4rem"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
<span className="flex px-2 py-1.5 group dark:text-gray-300 z-9 relative break-all">
|
||||
<Link
|
||||
@ -62,7 +67,8 @@ export default class ExitPages extends React.Component {
|
||||
</a>
|
||||
</span>
|
||||
</Bar>
|
||||
<span className="font-medium dark:text-gray-200">{numberFormatter(page.count)}</span>
|
||||
<span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(page.count)}</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(page.conversion_rate)}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -73,7 +79,10 @@ export default class ExitPages extends React.Component {
|
||||
<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>Page url</span>
|
||||
<span>Unique Exits</span>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">Unique Exits</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlipMove>
|
||||
|
@ -29,6 +29,10 @@ export default class Visits extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
fetchPages() {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/pages`, this.props.query)
|
||||
.then((res) => this.setState({loading: false, pages: res}))
|
||||
@ -39,6 +43,7 @@ export default class Visits extends React.Component {
|
||||
query.set('page', page.name)
|
||||
const domain = new URL('https://' + this.props.site.domain)
|
||||
const externalLink = 'https://' + domain.host + page.name
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -49,7 +54,7 @@ export default class Visits extends React.Component {
|
||||
count={page.count}
|
||||
all={this.state.pages}
|
||||
bg="bg-orange-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction="4rem"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
<span
|
||||
className="flex px-2 py-1.5 group dark:text-gray-300 relative z-9 break-all"
|
||||
@ -69,7 +74,8 @@ export default class Visits extends React.Component {
|
||||
</a>
|
||||
</span>
|
||||
</Bar>
|
||||
<span className="font-medium dark:text-gray-200">{numberFormatter(page.count)}</span>
|
||||
<span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(page.count)}</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(page.conversion_rate)}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -89,7 +95,10 @@ export default class Visits extends React.Component {
|
||||
<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>Page url</span>
|
||||
<span>{ this.label() }</span>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">{ this.label() }</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlipMove>
|
||||
|
@ -33,15 +33,21 @@ class AllSources extends React.Component {
|
||||
return this.props.query.period === 'realtime'
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
fetchReferrers() {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/sources`, this.props.query, {show_noref: this.showNoRef()})
|
||||
.then((res) => this.setState({loading: false, referrers: res}))
|
||||
.then((res) => this.setState({loading: false, referrers: res}))
|
||||
}
|
||||
|
||||
renderReferrer(referrer) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('source', referrer.name)
|
||||
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between my-1 text-sm"
|
||||
@ -51,7 +57,7 @@ class AllSources extends React.Component {
|
||||
count={referrer.count}
|
||||
all={this.state.referrers}
|
||||
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction="4rem"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
<span className="flex px-2 py-1.5 dark:text-gray-300 relative z-9 break-all">
|
||||
<Link
|
||||
@ -66,7 +72,8 @@ class AllSources extends React.Component {
|
||||
</Link>
|
||||
</span>
|
||||
</Bar>
|
||||
<span className="font-medium dark:text-gray-200">{numberFormatter(referrer.count)}</span>
|
||||
<span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(referrer.count)}</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{referrer.conversion_rate}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -81,7 +88,10 @@ class AllSources extends React.Component {
|
||||
<React.Fragment>
|
||||
<div className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500">
|
||||
<span>Source</span>
|
||||
<span>{this.label()}</span>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">{this.label()}</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlipMove className="flex-grow">
|
||||
@ -149,6 +159,10 @@ class UTMSources extends React.Component {
|
||||
return this.props.query.period === 'realtime'
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
fetchReferrers() {
|
||||
const endpoint = UTM_TAGS[this.props.tab].endpoint
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/${endpoint}`, this.props.query, {show_noref: this.showNoRef()})
|
||||
@ -158,6 +172,7 @@ class UTMSources extends React.Component {
|
||||
renderReferrer(referrer) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set(this.props.tab, referrer.name)
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -168,7 +183,7 @@ class UTMSources extends React.Component {
|
||||
count={referrer.count}
|
||||
all={this.state.referrers}
|
||||
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction="4rem"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
|
||||
<span className="flex px-2 py-1.5 dark:text-gray-300 relative z-9 break-all">
|
||||
@ -180,7 +195,8 @@ class UTMSources extends React.Component {
|
||||
</Link>
|
||||
</span>
|
||||
</Bar>
|
||||
<span className="font-medium dark:text-gray-200">{numberFormatter(referrer.count)}</span>
|
||||
<span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(referrer.count)}</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{referrer.conversion_rate}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -195,7 +211,10 @@ class UTMSources extends React.Component {
|
||||
<div className="flex flex-col flex-grow">
|
||||
<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>{UTM_TAGS[this.props.tab].label}</span>
|
||||
<span>{this.label()}</span>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">{this.label()}</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlipMove className="flex-grow">
|
||||
|
@ -213,6 +213,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
res =
|
||||
Stats.breakdown(site, query, "visit:source", metrics, pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "source", "visit:source")
|
||||
|> transform_keys(%{"source" => "name", "visitors" => "count"})
|
||||
|
||||
json(conn, res)
|
||||
@ -231,6 +232,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
res =
|
||||
Stats.breakdown(site, query, "visit:utm_medium", metrics, pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "utm_medium", "visit:utm_medium")
|
||||
|> transform_keys(%{"utm_medium" => "name", "visitors" => "count"})
|
||||
|
||||
json(conn, res)
|
||||
@ -249,6 +251,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
res =
|
||||
Stats.breakdown(site, query, "visit:utm_campaign", metrics, pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "utm_campaign", "visit:utm_campaign")
|
||||
|> transform_keys(%{"utm_campaign" => "name", "visitors" => "count"})
|
||||
|
||||
json(conn, res)
|
||||
@ -267,6 +270,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
res =
|
||||
Stats.breakdown(site, query, "visit:utm_source", metrics, pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "utm_source", "visit:utm_source")
|
||||
|> transform_keys(%{"utm_source" => "name", "visitors" => "count"})
|
||||
|
||||
json(conn, res)
|
||||
@ -332,6 +336,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
pages =
|
||||
Stats.breakdown(site, query, "event:page", metrics, pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "page", "event:page")
|
||||
|> transform_keys(%{"page" => "name", "visitors" => "count"})
|
||||
|
||||
json(conn, pages)
|
||||
@ -345,6 +350,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
entry_pages =
|
||||
Stats.breakdown(site, query, "visit:entry_page", metrics, pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "entry_page", "visit:entry_page")
|
||||
|> transform_keys(%{"entry_page" => "name", "visits" => "entries", "visitors" => "count"})
|
||||
|
||||
json(conn, entry_pages)
|
||||
@ -358,6 +364,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
exit_pages =
|
||||
Stats.breakdown(site, query, "visit:exit_page", metrics, {limit, page})
|
||||
|> maybe_add_cr(site, query, {limit, page}, "exit_page", "visit:exit_page")
|
||||
|> transform_keys(%{"exit_page" => "name", "visits" => "exits", "visitors" => "count"})
|
||||
|
||||
pages = Enum.map(exit_pages, & &1["name"])
|
||||
@ -395,6 +402,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
countries =
|
||||
Stats.breakdown(site, query, "visit:country", ["visitors"], {300, 1})
|
||||
|> maybe_add_cr(site, query, {300, 1}, "country", "visit:country")
|
||||
|> transform_keys(%{"country" => "name", "visitors" => "count"})
|
||||
|> Enum.map(fn country ->
|
||||
alpha3 = Stats.CountryName.to_alpha3(country["name"])
|
||||
@ -412,6 +420,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
browsers =
|
||||
Stats.breakdown(site, query, "visit:browser", ["visitors"], pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "browser", "visit:browser")
|
||||
|> transform_keys(%{"browser" => "name", "visitors" => "count"})
|
||||
|> add_percentages
|
||||
|
||||
@ -425,6 +434,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
versions =
|
||||
Stats.breakdown(site, query, "visit:browser_version", ["visitors"], pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "browser_version", "visit:browser_version")
|
||||
|> transform_keys(%{"browser_version" => "name", "visitors" => "count"})
|
||||
|> add_percentages
|
||||
|
||||
@ -438,6 +448,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
systems =
|
||||
Stats.breakdown(site, query, "visit:os", ["visitors"], pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "os", "visit:os")
|
||||
|> transform_keys(%{"os" => "name", "visitors" => "count"})
|
||||
|> add_percentages
|
||||
|
||||
@ -451,6 +462,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
versions =
|
||||
Stats.breakdown(site, query, "visit:os_version", ["visitors"], pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "os_version", "visit:os_version")
|
||||
|> transform_keys(%{"os_version" => "name", "visitors" => "count"})
|
||||
|> add_percentages
|
||||
|
||||
@ -464,6 +476,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
sizes =
|
||||
Stats.breakdown(site, query, "visit:device", ["visitors"], pagination)
|
||||
|> maybe_add_cr(site, query, pagination, "device", "visit:device")
|
||||
|> transform_keys(%{"device" => "name", "visitors" => "count"})
|
||||
|> add_percentages
|
||||
|
||||
@ -580,4 +593,32 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp add_cr(list, list_without_goals, key_name) do
|
||||
Enum.map(list, fn item ->
|
||||
without_goal = Enum.find(list_without_goals, fn s -> s[key_name] === item[key_name] end)
|
||||
|
||||
item
|
||||
|> Map.put(:conversion_rate, calculate_cr(without_goal["visitors"], item["visitors"]))
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_add_cr(list, site, query, pagination, key_name, filter_name) do
|
||||
if Map.has_key?(query.filters, "event:goal") do
|
||||
items = Enum.map(list, fn item -> item[key_name] end)
|
||||
|
||||
query_without_goal =
|
||||
query
|
||||
|> Query.put_filter(filter_name, {:member, items})
|
||||
|> Query.remove_goal()
|
||||
|
||||
res_without_goal =
|
||||
Stats.breakdown(site, query_without_goal, filter_name, ["visitors"], pagination)
|
||||
|
||||
list
|
||||
|> add_cr(res_without_goal, key_name)
|
||||
else
|
||||
list
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -19,6 +19,22 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
%{"name" => "Firefox", "count" => 1, "percentage" => 33}
|
||||
]
|
||||
end
|
||||
|
||||
test "calculates conversion_rate when filtering for goal", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview, user_id: 1, browser: "Chrome"),
|
||||
build(:pageview, user_id: 2, browser: "Chrome"),
|
||||
build(:event, user_id: 1, name: "Signup")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"goal" => "Signup"})
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "Chrome", "count" => 1, "percentage" => 100, "conversion_rate" => 50.0}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/browser-versions" do
|
||||
|
@ -33,5 +33,43 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "calculates conversion_rate when filtering for goal", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview,
|
||||
user_id: 1,
|
||||
country_code: "EE"
|
||||
),
|
||||
build(:event, user_id: 1, name: "Signup"),
|
||||
build(:pageview,
|
||||
user_id: 2,
|
||||
country_code: "EE"
|
||||
),
|
||||
build(:pageview,
|
||||
user_id: 3,
|
||||
country_code: "GB"
|
||||
),
|
||||
build(:event, user_id: 3, name: "Signup")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"goal" => "Signup"})
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"name" => "GBR",
|
||||
"count" => 1,
|
||||
"percentage" => 50,
|
||||
"conversion_rate" => 100.0
|
||||
},
|
||||
%{
|
||||
"name" => "EST",
|
||||
"count" => 1,
|
||||
"percentage" => 50,
|
||||
"conversion_rate" => 50.0
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -19,6 +19,23 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
%{"name" => "Android", "count" => 1, "percentage" => 33}
|
||||
]
|
||||
end
|
||||
|
||||
test "calculates conversion_rate when filtering for goal", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview, user_id: 1, operating_system: "Mac"),
|
||||
build(:pageview, user_id: 2, operating_system: "Mac"),
|
||||
build(:event, user_id: 1, name: "Signup")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"goal" => "Signup"})
|
||||
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "Mac", "count" => 1, "percentage" => 100, "conversion_rate" => 50.0}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/operating-system-versions" do
|
||||
|
@ -81,6 +81,23 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
%{"count" => 1, "name" => "/page2"}
|
||||
]
|
||||
end
|
||||
|
||||
test "calculates conversion_rate when filtering for goal", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview, user_id: 1, pathname: "/"),
|
||||
build(:pageview, user_id: 2, pathname: "/"),
|
||||
build(:pageview, user_id: 3, pathname: "/"),
|
||||
build(:event, user_id: 3, name: "Signup")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"goal" => "Signup"})
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"count" => 1, "name" => "/", "conversion_rate" => 33.3}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/entry-pages" do
|
||||
@ -133,6 +150,66 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "calculates conversion_rate when filtering for goal", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview,
|
||||
pathname: "/page1",
|
||||
user_id: 1,
|
||||
timestamp: ~N[2021-01-01 00:00:00]
|
||||
),
|
||||
build(:pageview,
|
||||
pathname: "/page1",
|
||||
user_id: 2,
|
||||
timestamp: ~N[2021-01-01 00:00:00]
|
||||
),
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
user_id: 1,
|
||||
timestamp: ~N[2021-01-01 00:00:00]
|
||||
),
|
||||
build(:pageview,
|
||||
pathname: "/page2",
|
||||
user_id: 3,
|
||||
timestamp: ~N[2021-01-01 00:00:00]
|
||||
),
|
||||
build(:pageview,
|
||||
pathname: "/page2",
|
||||
user_id: 3,
|
||||
timestamp: ~N[2021-01-01 00:15:00]
|
||||
),
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
user_id: 3,
|
||||
timestamp: ~N[2021-01-01 00:15:00]
|
||||
)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"goal" => "Signup"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"count" => 1,
|
||||
"entries" => 1,
|
||||
"name" => "/page2",
|
||||
"visit_duration" => 900,
|
||||
"conversion_rate" => 100.0
|
||||
},
|
||||
%{
|
||||
"count" => 1,
|
||||
"entries" => 1,
|
||||
"name" => "/page1",
|
||||
"visit_duration" => 0,
|
||||
"conversion_rate" => 50.0
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/exit-pages" do
|
||||
@ -168,7 +245,10 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "calculates correct exit rate when filtering for goal", %{conn: conn, site: site} do
|
||||
test "calculates correct exit rate and conversion_rate when filtering for goal", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
@ -206,8 +286,20 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "/exit1", "count" => 1, "exits" => 1, "exit_rate" => 50},
|
||||
%{"name" => "/exit2", "count" => 1, "exits" => 1, "exit_rate" => 100}
|
||||
%{
|
||||
"name" => "/exit1",
|
||||
"count" => 1,
|
||||
"exits" => 1,
|
||||
"exit_rate" => 50,
|
||||
"conversion_rate" => 100.0
|
||||
},
|
||||
%{
|
||||
"name" => "/exit2",
|
||||
"count" => 1,
|
||||
"exits" => 1,
|
||||
"exit_rate" => 100,
|
||||
"conversion_rate" => 100.0
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -19,5 +19,26 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
%{"name" => "Laptop", "count" => 1, "percentage" => 33}
|
||||
]
|
||||
end
|
||||
|
||||
test "calculates conversion_rate when filtering for goal", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview, user_id: 1, screen_size: "Desktop"),
|
||||
build(:pageview, user_id: 2, screen_size: "Desktop"),
|
||||
build(:event, user_id: 1, name: "Signup")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"goal" => "Signup"})
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"name" => "Desktop",
|
||||
"count" => 1,
|
||||
"percentage" => 100,
|
||||
"conversion_rate" => 50.0
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -283,7 +283,10 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
describe "GET /api/stats/:domain/sources - with goal filter" do
|
||||
setup [:create_user, :log_in, :create_new_site]
|
||||
|
||||
test "returns top referrers for a custom goal", %{conn: conn, site: site} do
|
||||
test "returns top referrers for a custom goal including conversion_rate", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
populate_stats(site, [
|
||||
build(:pageview,
|
||||
referrer_source: "Twitter",
|
||||
@ -307,11 +310,14 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "Twitter", "count" => 1}
|
||||
%{"name" => "Twitter", "count" => 1, "conversion_rate" => 50.0}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns top referrers for a pageview goal", %{conn: conn, site: site} do
|
||||
test "returns top referrers for a pageview goal including conversion_rate", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
populate_stats(site, [
|
||||
build(:pageview,
|
||||
referrer_source: "Twitter",
|
||||
@ -335,7 +341,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "Twitter", "count" => 1}
|
||||
%{"name" => "Twitter", "count" => 1, "conversion_rate" => 50.0}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user