mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 17:44:43 +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
|
- 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
|
- Ability to invite users to sites with different roles plausible/analytics#1122
|
||||||
- Option to configure a custom name for the script file
|
- 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
|
### Fixed
|
||||||
- Fix weekly report time range plausible/analytics#951
|
- Fix weekly report time range plausible/analytics#951
|
||||||
|
@ -21,7 +21,7 @@ export default class Browsers extends React.Component {
|
|||||||
this.fetchBrowsers()
|
this.fetchBrowsers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisible() {
|
onVisible() {
|
||||||
this.fetchBrowsers()
|
this.fetchBrowsers()
|
||||||
if (this.props.timer) this.props.timer.onTick(this.fetchBrowsers.bind(this))
|
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() {
|
label() {
|
||||||
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||||
}
|
}
|
||||||
@ -58,6 +62,7 @@ export default class Browsers extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
query.set('browser', browser.name)
|
query.set('browser', browser.name)
|
||||||
}
|
}
|
||||||
|
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between my-1 text-sm" key={browser.name}>
|
<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}
|
count={browser.count}
|
||||||
all={this.state.browsers}
|
all={this.state.browsers}
|
||||||
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
|
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||||
maxWidthDeduction="6rem"
|
maxWidthDeduction={maxWidthDeduction}
|
||||||
>
|
>
|
||||||
{this.renderBrowserContent(browser, query)}
|
{this.renderBrowserContent(browser, query)}
|
||||||
</Bar>
|
</Bar>
|
||||||
<span className="font-medium dark:text-gray-200">
|
<span className="font-medium dark:text-gray-200 text-right w-20">
|
||||||
{numberFormatter(browser.count)}
|
{numberFormatter(browser.count)} <span className="inline-block w-8 text-xs"> ({browser.percentage}%)</span>
|
||||||
<span className="inline-block w-8 text-xs text-right">({browser.percentage}%)</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(browser.conversion_rate)}%</span>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -85,7 +90,10 @@ export default class Browsers extends React.Component {
|
|||||||
<React.Fragment>
|
<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">
|
<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>{ 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>
|
</div>
|
||||||
{ this.state.browsers && this.state.browsers.map(this.renderBrowser.bind(this)) }
|
{ this.state.browsers && this.state.browsers.map(this.renderBrowser.bind(this)) }
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -57,7 +57,6 @@ class ScreenSizes extends React.Component {
|
|||||||
if (this.props.timer) this.props.timer.onTick(this.fetchScreenSizes.bind(this))
|
if (this.props.timer) this.props.timer.onTick(this.fetchScreenSizes.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fetchScreenSizes() {
|
fetchScreenSizes() {
|
||||||
api.get(
|
api.get(
|
||||||
`/api/stats/${encodeURIComponent(this.props.site.domain)}/screen-sizes`,
|
`/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}))
|
.then((res) => this.setState({loading: false, sizes: res}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.props.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
label() {
|
label() {
|
||||||
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||||
}
|
}
|
||||||
@ -73,6 +77,7 @@ class ScreenSizes extends React.Component {
|
|||||||
renderScreenSize(size) {
|
renderScreenSize(size) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
query.set('screen', size.name)
|
query.set('screen', size.name)
|
||||||
|
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between my-1 text-sm" key={size.name}>
|
<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}
|
count={size.count}
|
||||||
all={this.state.sizes}
|
all={this.state.sizes}
|
||||||
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
|
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||||
maxWidthDeduction="6rem"
|
maxWidthDeduction={maxWidthDeduction}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
tooltip={EXPLANATION[size.name]}
|
tooltip={EXPLANATION[size.name]}
|
||||||
@ -91,12 +96,10 @@ class ScreenSizes extends React.Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
</Bar>
|
</Bar>
|
||||||
<span
|
<span className="font-medium dark:text-gray-200 text-right w-20">
|
||||||
className="font-medium dark:text-gray-200"
|
{numberFormatter(size.count)} <span className="inline-block w-8 text-xs text-right">({size.percentage}%)</span>
|
||||||
>
|
|
||||||
{numberFormatter(size.count)}
|
|
||||||
<span className="inline-block w-8 text-xs text-right">({size.percentage}%)</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(size.conversion_rate)}%</span>}
|
||||||
</div>
|
</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"
|
className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500"
|
||||||
>
|
>
|
||||||
<span>Screen size</span>
|
<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>
|
</div>
|
||||||
{ this.state.sizes && this.state.sizes.map(this.renderScreenSize.bind(this)) }
|
{ this.state.sizes && this.state.sizes.map(this.renderScreenSize.bind(this)) }
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@ -146,14 +152,14 @@ export default class Devices extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setMode(mode) {
|
setMode(mode) {
|
||||||
return () => {
|
return () => {
|
||||||
storage.setItem(this.tabKey, mode)
|
storage.setItem(this.tabKey, mode)
|
||||||
this.setState({mode})
|
this.setState({mode})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
switch (this.state.mode) {
|
switch (this.state.mode) {
|
||||||
case 'browser':
|
case 'browser':
|
||||||
@ -190,7 +196,7 @@ export default class Devices extends React.Component {
|
|||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className="cursor-pointer hover:text-indigo-600"
|
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) {
|
renderOperatingSystem(os) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
if (this.props.query.filters.os) {
|
if (this.props.query.filters.os) {
|
||||||
@ -43,6 +47,7 @@ export default class OperatingSystems extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
query.set('os', os.name)
|
query.set('os', os.name)
|
||||||
}
|
}
|
||||||
|
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -53,7 +58,7 @@ export default class OperatingSystems extends React.Component {
|
|||||||
count={os.count}
|
count={os.count}
|
||||||
all={this.state.operatingSystems}
|
all={this.state.operatingSystems}
|
||||||
bg="bg-green-50 dark:gray-500 dark:bg-opacity-15"
|
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">
|
<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()}}>
|
<Link className="md:truncate block hover:underline" to={{search: query.toString()}}>
|
||||||
@ -61,7 +66,8 @@ export default class OperatingSystems extends React.Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
</Bar>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -78,7 +84,10 @@ export default class OperatingSystems extends React.Component {
|
|||||||
<React.Fragment>
|
<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">
|
<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>{ 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>
|
</div>
|
||||||
{ this.state.operatingSystems && this.state.operatingSystems.map(this.renderOperatingSystem.bind(this)) }
|
{ this.state.operatingSystems && this.state.operatingSystems.map(this.renderOperatingSystem.bind(this)) }
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -25,6 +25,10 @@ class CountriesModal extends React.Component {
|
|||||||
return this.state.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
return this.state.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.state.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
renderCountry(country) {
|
renderCountry(country) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
query.set('country', country.name)
|
query.set('country', country.name)
|
||||||
@ -47,6 +51,7 @@ class CountriesModal extends React.Component {
|
|||||||
<td className="p-2 w-32 font-medium" align="right">
|
<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>
|
{numberFormatter(country.count)} <span className="inline-block text-xs w-8 text-right">({country.percentage}%)</span>
|
||||||
</td>
|
</td>
|
||||||
|
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{country.conversion_rate}%</td> }
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -57,7 +62,7 @@ class CountriesModal extends React.Component {
|
|||||||
<div className="loading mt-32 mx-auto"><div></div></div>
|
<div className="loading mt-32 mx-auto"><div></div></div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.countries) {
|
if (this.state.countries) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -81,6 +86,7 @@ class CountriesModal extends React.Component {
|
|||||||
>
|
>
|
||||||
{this.label()}
|
{this.label()}
|
||||||
</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">CR</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -56,6 +56,10 @@ class EntryPagesModal extends React.Component {
|
|||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.state.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
renderPage(page) {
|
renderPage(page) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
query.set('entry_page', page.name)
|
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.count)}</td>
|
||||||
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.entries)}</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.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>
|
</tr>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -125,6 +130,11 @@ class EntryPagesModal extends React.Component {
|
|||||||
align="right"
|
align="right"
|
||||||
>Visit Duration
|
>Visit Duration
|
||||||
</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"
|
||||||
|
>CR
|
||||||
|
</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -42,6 +42,10 @@ class ExitPagesModal extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.state.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
renderPage(page) {
|
renderPage(page) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
query.set('exit_page', page.name)
|
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.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">{numberFormatter(page.exits)}</td>
|
||||||
<td className="p-2 w-32 font-medium" align="right">{this.formatPercentage(page.exit_rate)}</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>
|
</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">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">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>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -44,6 +44,10 @@ class PagesModal extends React.Component {
|
|||||||
return this.state.query.period !== 'realtime' && !(filters.goal || filters.source || filters.referrer)
|
return this.state.query.period !== 'realtime' && !(filters.goal || filters.source || filters.referrer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.state.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
formatBounceRate(page) {
|
formatBounceRate(page) {
|
||||||
if (typeof(page.bounce_rate) === 'number') {
|
if (typeof(page.bounce_rate) === 'number') {
|
||||||
return page.bounce_rate + '%'
|
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.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">{this.formatBounceRate(page)}</td> }
|
||||||
{this.showExtra() && <td className="p-2 w-32 font-medium" align="right">{timeOnPage}</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>
|
</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.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">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.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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -54,6 +54,10 @@ class SourcesModal extends React.Component {
|
|||||||
return this.state.query.period !== 'realtime' && !this.state.query.filters.goal
|
return this.state.query.period !== 'realtime' && !this.state.query.filters.goal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.state.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
loadMore() {
|
loadMore() {
|
||||||
this.setState({loading: true, page: this.state.page + 1}, this.loadSources.bind(this))
|
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_sources') query.set('utm_source', source.name)
|
||||||
if (filter === 'utm_campaigns') query.set('utm_campaign', source.name)
|
if (filter === 'utm_campaigns') query.set('utm_campaign', source.name)
|
||||||
|
|
||||||
|
console.log(source)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="text-sm dark:text-gray-200" key={source.name}>
|
<tr className="text-sm dark:text-gray-200" key={source.name}>
|
||||||
<td className="p-2">
|
<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>
|
<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.formatBounceRate(source)}</td> }
|
||||||
{this.showExtra() && <td className="p-2 w-32 font-medium" align="right">{this.formatDuration(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>
|
</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>
|
<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">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.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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -29,6 +29,10 @@ export default class EntryPages extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.props.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
fetchPages() {
|
fetchPages() {
|
||||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/entry-pages`, this.props.query)
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/entry-pages`, this.props.query)
|
||||||
.then((res) => this.setState({loading: false, pages: res}))
|
.then((res) => this.setState({loading: false, pages: res}))
|
||||||
@ -37,6 +41,7 @@ export default class EntryPages extends React.Component {
|
|||||||
renderPage(page) {
|
renderPage(page) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
query.set('entry_page', page.name)
|
query.set('entry_page', page.name)
|
||||||
|
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between my-1 text-sm" key={page.name}>
|
<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}
|
count={page.count}
|
||||||
all={this.state.pages}
|
all={this.state.pages}
|
||||||
bg="bg-orange-50 dark:bg-gray-500 dark:bg-opacity-15"
|
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">
|
<span className="flex px-2 py-1.5 group dark:text-gray-300 relative break-all z-9">
|
||||||
<Link
|
<Link
|
||||||
@ -62,7 +67,8 @@ export default class EntryPages extends React.Component {
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</Bar>
|
</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>
|
</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">
|
<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>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>
|
</div>
|
||||||
|
|
||||||
<FlipMove>
|
<FlipMove>
|
||||||
@ -81,7 +90,7 @@ export default class EntryPages extends React.Component {
|
|||||||
</FlipMove>
|
</FlipMove>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400"
|
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() {
|
fetchPages() {
|
||||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/exit-pages`, this.props.query)
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/exit-pages`, this.props.query)
|
||||||
.then((res) => this.setState({loading: false, pages: res}))
|
.then((res) => this.setState({loading: false, pages: res}))
|
||||||
@ -37,6 +41,7 @@ export default class ExitPages extends React.Component {
|
|||||||
renderPage(page) {
|
renderPage(page) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
query.set('exit_page', page.name)
|
query.set('exit_page', page.name)
|
||||||
|
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between my-1 text-sm" key={page.name}>
|
<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}
|
count={page.count}
|
||||||
all={this.state.pages}
|
all={this.state.pages}
|
||||||
bg="bg-orange-50 dark:bg-gray-500 dark:bg-opacity-15"
|
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">
|
<span className="flex px-2 py-1.5 group dark:text-gray-300 z-9 relative break-all">
|
||||||
<Link
|
<Link
|
||||||
@ -62,7 +67,8 @@ export default class ExitPages extends React.Component {
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</Bar>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -73,7 +79,10 @@ export default class ExitPages extends React.Component {
|
|||||||
<React.Fragment>
|
<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">
|
<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>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>
|
</div>
|
||||||
|
|
||||||
<FlipMove>
|
<FlipMove>
|
||||||
|
@ -29,6 +29,10 @@ export default class Visits extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.props.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
fetchPages() {
|
fetchPages() {
|
||||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/pages`, this.props.query)
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/pages`, this.props.query)
|
||||||
.then((res) => this.setState({loading: false, pages: res}))
|
.then((res) => this.setState({loading: false, pages: res}))
|
||||||
@ -39,6 +43,7 @@ export default class Visits extends React.Component {
|
|||||||
query.set('page', page.name)
|
query.set('page', page.name)
|
||||||
const domain = new URL('https://' + this.props.site.domain)
|
const domain = new URL('https://' + this.props.site.domain)
|
||||||
const externalLink = 'https://' + domain.host + page.name
|
const externalLink = 'https://' + domain.host + page.name
|
||||||
|
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -49,7 +54,7 @@ export default class Visits extends React.Component {
|
|||||||
count={page.count}
|
count={page.count}
|
||||||
all={this.state.pages}
|
all={this.state.pages}
|
||||||
bg="bg-orange-50 dark:bg-gray-500 dark:bg-opacity-15"
|
bg="bg-orange-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||||
maxWidthDeduction="4rem"
|
maxWidthDeduction={maxWidthDeduction}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="flex px-2 py-1.5 group dark:text-gray-300 relative z-9 break-all"
|
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>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</Bar>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -89,7 +95,10 @@ export default class Visits extends React.Component {
|
|||||||
<React.Fragment>
|
<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">
|
<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>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>
|
</div>
|
||||||
|
|
||||||
<FlipMove>
|
<FlipMove>
|
||||||
|
@ -33,15 +33,21 @@ class AllSources extends React.Component {
|
|||||||
return this.props.query.period === 'realtime'
|
return this.props.query.period === 'realtime'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.props.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
fetchReferrers() {
|
fetchReferrers() {
|
||||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/sources`, this.props.query, {show_noref: this.showNoRef()})
|
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) {
|
renderReferrer(referrer) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
query.set('source', referrer.name)
|
query.set('source', referrer.name)
|
||||||
|
|
||||||
|
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between my-1 text-sm"
|
className="flex items-center justify-between my-1 text-sm"
|
||||||
@ -51,7 +57,7 @@ class AllSources extends React.Component {
|
|||||||
count={referrer.count}
|
count={referrer.count}
|
||||||
all={this.state.referrers}
|
all={this.state.referrers}
|
||||||
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
|
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">
|
<span className="flex px-2 py-1.5 dark:text-gray-300 relative z-9 break-all">
|
||||||
<Link
|
<Link
|
||||||
@ -66,7 +72,8 @@ class AllSources extends React.Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
</Bar>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -81,7 +88,10 @@ class AllSources extends React.Component {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500">
|
<div className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500">
|
||||||
<span>Source</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<FlipMove className="flex-grow">
|
<FlipMove className="flex-grow">
|
||||||
@ -149,6 +159,10 @@ class UTMSources extends React.Component {
|
|||||||
return this.props.query.period === 'realtime'
|
return this.props.query.period === 'realtime'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConversionRate() {
|
||||||
|
return !!this.props.query.filters.goal
|
||||||
|
}
|
||||||
|
|
||||||
fetchReferrers() {
|
fetchReferrers() {
|
||||||
const endpoint = UTM_TAGS[this.props.tab].endpoint
|
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()})
|
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) {
|
renderReferrer(referrer) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
query.set(this.props.tab, referrer.name)
|
query.set(this.props.tab, referrer.name)
|
||||||
|
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -168,7 +183,7 @@ class UTMSources extends React.Component {
|
|||||||
count={referrer.count}
|
count={referrer.count}
|
||||||
all={this.state.referrers}
|
all={this.state.referrers}
|
||||||
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
|
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">
|
<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>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
</Bar>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -195,7 +211,10 @@ class UTMSources extends React.Component {
|
|||||||
<div className="flex flex-col flex-grow">
|
<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">
|
<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>{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>
|
</div>
|
||||||
|
|
||||||
<FlipMove className="flex-grow">
|
<FlipMove className="flex-grow">
|
||||||
|
@ -213,6 +213,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:source", metrics, pagination)
|
Stats.breakdown(site, query, "visit:source", metrics, pagination)
|
||||||
|
|> maybe_add_cr(site, query, pagination, "source", "visit:source")
|
||||||
|> transform_keys(%{"source" => "name", "visitors" => "count"})
|
|> transform_keys(%{"source" => "name", "visitors" => "count"})
|
||||||
|
|
||||||
json(conn, res)
|
json(conn, res)
|
||||||
@ -231,6 +232,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:utm_medium", metrics, pagination)
|
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"})
|
|> transform_keys(%{"utm_medium" => "name", "visitors" => "count"})
|
||||||
|
|
||||||
json(conn, res)
|
json(conn, res)
|
||||||
@ -249,6 +251,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:utm_campaign", metrics, pagination)
|
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"})
|
|> transform_keys(%{"utm_campaign" => "name", "visitors" => "count"})
|
||||||
|
|
||||||
json(conn, res)
|
json(conn, res)
|
||||||
@ -267,6 +270,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
res =
|
res =
|
||||||
Stats.breakdown(site, query, "visit:utm_source", metrics, pagination)
|
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"})
|
|> transform_keys(%{"utm_source" => "name", "visitors" => "count"})
|
||||||
|
|
||||||
json(conn, res)
|
json(conn, res)
|
||||||
@ -332,6 +336,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
pages =
|
pages =
|
||||||
Stats.breakdown(site, query, "event:page", metrics, pagination)
|
Stats.breakdown(site, query, "event:page", metrics, pagination)
|
||||||
|
|> maybe_add_cr(site, query, pagination, "page", "event:page")
|
||||||
|> transform_keys(%{"page" => "name", "visitors" => "count"})
|
|> transform_keys(%{"page" => "name", "visitors" => "count"})
|
||||||
|
|
||||||
json(conn, pages)
|
json(conn, pages)
|
||||||
@ -345,6 +350,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
entry_pages =
|
entry_pages =
|
||||||
Stats.breakdown(site, query, "visit:entry_page", metrics, pagination)
|
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"})
|
|> transform_keys(%{"entry_page" => "name", "visits" => "entries", "visitors" => "count"})
|
||||||
|
|
||||||
json(conn, entry_pages)
|
json(conn, entry_pages)
|
||||||
@ -358,6 +364,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
exit_pages =
|
exit_pages =
|
||||||
Stats.breakdown(site, query, "visit:exit_page", metrics, {limit, page})
|
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"})
|
|> transform_keys(%{"exit_page" => "name", "visits" => "exits", "visitors" => "count"})
|
||||||
|
|
||||||
pages = Enum.map(exit_pages, & &1["name"])
|
pages = Enum.map(exit_pages, & &1["name"])
|
||||||
@ -395,6 +402,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
countries =
|
countries =
|
||||||
Stats.breakdown(site, query, "visit:country", ["visitors"], {300, 1})
|
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"})
|
|> transform_keys(%{"country" => "name", "visitors" => "count"})
|
||||||
|> Enum.map(fn country ->
|
|> Enum.map(fn country ->
|
||||||
alpha3 = Stats.CountryName.to_alpha3(country["name"])
|
alpha3 = Stats.CountryName.to_alpha3(country["name"])
|
||||||
@ -412,6 +420,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
browsers =
|
browsers =
|
||||||
Stats.breakdown(site, query, "visit:browser", ["visitors"], pagination)
|
Stats.breakdown(site, query, "visit:browser", ["visitors"], pagination)
|
||||||
|
|> maybe_add_cr(site, query, pagination, "browser", "visit:browser")
|
||||||
|> transform_keys(%{"browser" => "name", "visitors" => "count"})
|
|> transform_keys(%{"browser" => "name", "visitors" => "count"})
|
||||||
|> add_percentages
|
|> add_percentages
|
||||||
|
|
||||||
@ -425,6 +434,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
versions =
|
versions =
|
||||||
Stats.breakdown(site, query, "visit:browser_version", ["visitors"], pagination)
|
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"})
|
|> transform_keys(%{"browser_version" => "name", "visitors" => "count"})
|
||||||
|> add_percentages
|
|> add_percentages
|
||||||
|
|
||||||
@ -438,6 +448,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
systems =
|
systems =
|
||||||
Stats.breakdown(site, query, "visit:os", ["visitors"], pagination)
|
Stats.breakdown(site, query, "visit:os", ["visitors"], pagination)
|
||||||
|
|> maybe_add_cr(site, query, pagination, "os", "visit:os")
|
||||||
|> transform_keys(%{"os" => "name", "visitors" => "count"})
|
|> transform_keys(%{"os" => "name", "visitors" => "count"})
|
||||||
|> add_percentages
|
|> add_percentages
|
||||||
|
|
||||||
@ -451,6 +462,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
versions =
|
versions =
|
||||||
Stats.breakdown(site, query, "visit:os_version", ["visitors"], pagination)
|
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"})
|
|> transform_keys(%{"os_version" => "name", "visitors" => "count"})
|
||||||
|> add_percentages
|
|> add_percentages
|
||||||
|
|
||||||
@ -464,6 +476,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
sizes =
|
sizes =
|
||||||
Stats.breakdown(site, query, "visit:device", ["visitors"], pagination)
|
Stats.breakdown(site, query, "visit:device", ["visitors"], pagination)
|
||||||
|
|> maybe_add_cr(site, query, pagination, "device", "visit:device")
|
||||||
|> transform_keys(%{"device" => "name", "visitors" => "count"})
|
|> transform_keys(%{"device" => "name", "visitors" => "count"})
|
||||||
|> add_percentages
|
|> add_percentages
|
||||||
|
|
||||||
@ -580,4 +593,32 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
query
|
query
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
@ -19,6 +19,22 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
|||||||
%{"name" => "Firefox", "count" => 1, "percentage" => 33}
|
%{"name" => "Firefox", "count" => 1, "percentage" => 33}
|
||||||
]
|
]
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "GET /api/stats/:domain/browser-versions" do
|
describe "GET /api/stats/:domain/browser-versions" do
|
||||||
|
@ -33,5 +33,43 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
@ -19,6 +19,23 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
|||||||
%{"name" => "Android", "count" => 1, "percentage" => 33}
|
%{"name" => "Android", "count" => 1, "percentage" => 33}
|
||||||
]
|
]
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "GET /api/stats/:domain/operating-system-versions" do
|
describe "GET /api/stats/:domain/operating-system-versions" do
|
||||||
|
@ -81,6 +81,23 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
|||||||
%{"count" => 1, "name" => "/page2"}
|
%{"count" => 1, "name" => "/page2"}
|
||||||
]
|
]
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "GET /api/stats/:domain/entry-pages" do
|
describe "GET /api/stats/:domain/entry-pages" do
|
||||||
@ -133,6 +150,66 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "GET /api/stats/:domain/exit-pages" do
|
describe "GET /api/stats/:domain/exit-pages" do
|
||||||
@ -168,7 +245,10 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
|||||||
]
|
]
|
||||||
end
|
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, [
|
populate_stats(site, [
|
||||||
build(:event,
|
build(:event,
|
||||||
name: "Signup",
|
name: "Signup",
|
||||||
@ -206,8 +286,20 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200) == [
|
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
|
end
|
||||||
|
|
||||||
|
@ -19,5 +19,26 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
|||||||
%{"name" => "Laptop", "count" => 1, "percentage" => 33}
|
%{"name" => "Laptop", "count" => 1, "percentage" => 33}
|
||||||
]
|
]
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
@ -283,7 +283,10 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
|||||||
describe "GET /api/stats/:domain/sources - with goal filter" do
|
describe "GET /api/stats/:domain/sources - with goal filter" do
|
||||||
setup [:create_user, :log_in, :create_new_site]
|
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, [
|
populate_stats(site, [
|
||||||
build(:pageview,
|
build(:pageview,
|
||||||
referrer_source: "Twitter",
|
referrer_source: "Twitter",
|
||||||
@ -307,11 +310,14 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200) == [
|
assert json_response(conn, 200) == [
|
||||||
%{"name" => "Twitter", "count" => 1}
|
%{"name" => "Twitter", "count" => 1, "conversion_rate" => 50.0}
|
||||||
]
|
]
|
||||||
end
|
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, [
|
populate_stats(site, [
|
||||||
build(:pageview,
|
build(:pageview,
|
||||||
referrer_source: "Twitter",
|
referrer_source: "Twitter",
|
||||||
@ -335,7 +341,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200) == [
|
assert json_response(conn, 200) == [
|
||||||
%{"name" => "Twitter", "count" => 1}
|
%{"name" => "Twitter", "count" => 1, "conversion_rate" => 50.0}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user