Refactor: Use ListReport component in Sources (#3153)

* refactor SourceList to use ListReport

* refactor SourceList into a fn comp

* change referrer-drilldown API response format and remove dead code

* use ListReport in referrer-list

* fix CI

* fix flaky test

* remove IO.inspect
This commit is contained in:
RobertJoonas 2023-07-21 07:35:41 +03:00 committed by GitHub
parent dfa5bbf4a0
commit b9d122c0c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 216 additions and 514 deletions

View File

@ -19,7 +19,7 @@ class ReferrerDrilldownModal extends React.Component {
const detailed = this.showExtra()
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/${this.props.match.params.referrer}`, this.state.query, {limit: 100, detailed})
.then((res) => this.setState({loading: false, referrers: res.referrers, totalVisitors: res.total_visitors}))
.then((referrers) => this.setState({loading: false, referrers: referrers}))
}
showExtra() {

View File

@ -7,6 +7,7 @@ import FadeIn from '../../fade-in'
import MoreLink from '../more-link'
import Bar from '../bar'
import LazyLoader from '../../components/lazy-loader'
import classNames from 'classnames'
const MAX_ITEMS = 9
const MIN_HEIGHT = 380
@ -15,10 +16,29 @@ const ROW_GAP_HEIGHT = 4
const DATA_CONTAINER_HEIGHT = (ROW_HEIGHT + ROW_GAP_HEIGHT) * (MAX_ITEMS - 1) + ROW_HEIGHT
const COL_MIN_WIDTH = 70
function ExternalLink({item, externalLinkDest}) {
if (externalLinkDest) {
const dest = externalLinkDest(item)
function FilterLink({filterQuery, onClick, children}) {
const className = classNames('max-w-max w-full flex md:overflow-hidden', {
'hover:underline': !!filterQuery
})
if (filterQuery) {
return (
<Link
to={{search: filterQuery.toString()}}
onClick={onClick}
className={className}
>
{ children }
</Link>
)
} else {
return <span className={className}>{ children }</span>
}
}
function ExternalLink({item, externalLinkDest}) {
const dest = externalLinkDest && externalLinkDest(item)
if (dest) {
return (
<a
target="_blank"
@ -63,7 +83,8 @@ function ExternalLink({item, externalLinkDest}) {
// in realtime or goal-filtered views, we'll use `realtimeLabel` and `GoalFilterLabel`.
// * `getFilterFor` - a function that takes a list item and returns the query link (with
// the filter) to navigate to when this list item is clicked.
// the filter) to navigate to when this list item is clicked. If a list item is not
// supposed to be clickable, this function should return `null` for that list item.
// OPTIONAL PROPS:
@ -76,8 +97,8 @@ function ExternalLink({item, externalLinkDest}) {
// to navigate to. If this prop is given, an additional icon is rendered upon hovering
// the entry.
// * `renderIconFor` - a function that takes a list item and returns the
// HTML of an icon (such as a flag or screen size icon) for a listItem.
// * `renderIcon` - a function that takes a list item and returns the
// HTML of an icon (such as a flag, favicon, or a screen size icon) for a listItem.
// * `color` - color of the comparison bars in light-mode
@ -168,14 +189,19 @@ export default function ListReport(props) {
)
}
function renderBarFor(listItem) {
const query = new URLSearchParams(window.location.search)
function getFilterQuery(listItem) {
const filter = props.getFilterFor(listItem)
if (!filter) { return null }
const query = new URLSearchParams(window.location.search)
Object.entries(filter).forEach((([key, value]) => {
query.set(key, value)
}))
return query
}
function renderBarFor(listItem) {
const lightBackground = props.color || 'bg-green-50'
const noop = () => {}
const metricToPlot = metrics[0].name
@ -189,14 +215,14 @@ export default function ListReport(props) {
plot={metricToPlot}
>
<div className="flex justify-start px-2 py-1.5 group text-sm dark:text-gray-300 relative z-9 break-all w-full">
<Link onClick={props.onClick || noop} to={{search: query.toString()}} className="max-w-max w-full flex hover:underline md:overflow-hidden">
<FilterLink filterQuery={getFilterQuery(listItem)} onClick={props.onClick || noop}>
{maybeRenderIconFor(listItem)}
<span className="w-full md:truncate">
{listItem.name}
</span>
</Link>
<ExternalLink item={listItem} externalLinkDest={props.externalLinkDest} />
</FilterLink>
<ExternalLink item={listItem} externalLinkDest={props.externalLinkDest} />
</div>
</Bar>
</div>

View File

@ -1,177 +1,48 @@
import React from 'react';
import { Link } from 'react-router-dom'
import FlipMove from 'react-flip-move';
import FadeIn from '../../fade-in'
import Bar from '../bar'
import MoreLink from '../more-link'
import numberFormatter from '../../util/number-formatter'
import * as api from '../../api'
import LazyLoader from '../../components/lazy-loader'
import * as url from '../../util/url'
import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics'
import ListReport from '../reports/list'
function LinkOption(props) {
if (props.disabled) {
return <span {...props}>{props.children}</span>
} else {
props = Object.assign({}, props, { className: props.className + ' hover:underline' })
return <Link {...props}>{props.children}</Link>
}
}
export default class Referrers extends React.Component {
constructor(props) {
super(props)
this.state = { loading: true }
this.onVisible = this.onVisible.bind(this)
this.fetchReferrers = this.fetchReferrers.bind(this)
}
onVisible() {
this.fetchReferrers()
if (this.props.query.period === 'realtime') {
document.addEventListener('tick', this.fetchReferrers)
}
}
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.setState({ loading: true, referrers: null })
this.fetchReferrers()
}
}
componentWillUnmount() {
document.removeEventListener('tick', this.fetchReferrers)
}
showConversionRate() {
return !!this.props.query.filters.goal
}
fetchReferrers() {
if (this.props.query.filters.source) {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/${encodeURIComponent(this.props.query.filters.source)}`, this.props.query)
.then((res) => res.search_terms || res.referrers)
.then((referrers) => this.setState({ loading: false, referrers: referrers }))
} else if (this.props.query.filters.goal) {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/goal/referrers`, this.props.query)
.then((res) => this.setState({ loading: false, referrers: res }))
} else {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers`, this.props.query)
.then((res) => this.setState({ loading: false, referrers: res }))
}
}
renderExternalLink(referrer) {
if (this.props.query.filters.source && this.props.query.filters.source !== 'Google' && referrer.name !== 'Direct / None') {
return (
<a target="_blank" href={'//' + referrer.name} rel="noreferrer" className="hidden group-hover:block">
<svg className="inline w-4 h-4 ml-1 -mt-1 text-gray-600 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"></path><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"></path></svg>
</a>
)
}
return null
}
renderReferrer(referrer) {
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
const query = new URLSearchParams(window.location.search)
query.set('referrer', referrer.name)
return (
<div className="flex items-center justify-between my-1 text-sm" key={referrer.name}>
<Bar
count={referrer.visitors}
all={this.state.referrers}
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
maxWidthDeduction={maxWidthDeduction}
>
<span className="flex px-2 py-1.5 z-9 relative break-all group">
<LinkOption
className="block md:truncate dark:text-gray-300"
to={{ search: query.toString() }}
disabled={referrer.name === 'Direct / None'}
>
<img
src={`/favicon/sources/${encodeURIComponent(referrer.name)}`}
referrerPolicy="no-referrer"
className="inline w-4 h-4 mr-2 -mt-px align-middle"
/>
{referrer.name}
</LinkOption>
{this.renderExternalLink(referrer)}
</span>
</Bar>
<span className="font-medium dark:text-gray-200">{numberFormatter(referrer.visitors)}</span>
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{referrer.conversion_rate}%</span>}
</div>
)
}
label() {
if (this.props.query.period === 'realtime') {
return 'Current visitors'
}
if (this.showConversionRate()) {
return 'Conversions'
}
return 'Visitors'
}
renderList() {
if (this.state.referrers.length > 0) {
return (
<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"
>
<span>Referrer</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">
{this.state.referrers.map(this.renderReferrer.bind(this))}
</FlipMove>
</div>
)
}
return (
<div
className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400"
>
No data yet
</div>
)
}
renderContent() {
if (this.state.referrers) {
return (
<React.Fragment>
{this.renderList()}
<MoreLink site={this.props.site} list={this.state.referrers} endpoint={`referrers/${this.props.query.filters.source}`} className="w-full pb-4 absolute bottom-0 left-0"/>
</React.Fragment>
)
}
}
render() {
return (
<div className="flex flex-col flex-grow">
<LazyLoader onVisible={this.onVisible}>
<h3 className="font-bold dark:text-gray-100">Top Referrers</h3>
{this.state.loading && <div className="mx-auto loading mt-44"><div></div></div>}
<FadeIn show={!this.state.loading}>
{this.renderContent()}
</FadeIn>
</LazyLoader>
</div>
)
export default function Referrers({site, query}) {
function fetchReferrers() {
return api.get(url.apiPath(site, `/referrers/${encodeURIComponent(query.filters.source)}`), query, {limit: 9})
}
function externalLinkDest(referrer) {
if (referrer.name === 'Direct / None') { return null }
return `https://${referrer.name}`
}
function getFilterFor(referrer) {
if (referrer.name === 'Direct / None') { return null }
return { referrer: referrer.name }
}
function renderIcon(listItem) {
return (
<img
src={`/favicon/sources/${encodeURIComponent(listItem.name)}`}
referrerPolicy="no-referrer"
className="inline w-4 h-4 mr-2 -mt-px align-middle"
/>
)
}
return (
<div className="flex flex-col flex-grow">
<h3 className="font-bold dark:text-gray-100">Top Referrers</h3>
<ListReport
fetchData={fetchReferrers}
getFilterFor={getFilterFor}
keyLabel="Referrer"
metrics={maybeWithCR([VISITORS_METRIC], query)}
detailsLink={url.sitePath(site, `/referrers/${encodeURIComponent(query.filters.source)}`)}
query={query}
externalLinkDest={externalLinkDest}
renderIcon={renderIcon}
color="bg-blue-50"
/>
</div>
)
}

View File

@ -1,304 +1,108 @@
import React from 'react';
import { Link } from 'react-router-dom'
import FlipMove from 'react-flip-move';
import React, { Fragment, useState } from 'react';
import * as storage from '../../util/storage'
import FadeIn from '../../fade-in'
import Bar from '../bar'
import MoreLink from '../more-link'
import numberFormatter from '../../util/number-formatter'
import * as api from '../../api'
import * as url from '../../util/url'
import LazyLoader from '../../components/lazy-loader'
class AllSources extends React.Component {
constructor(props) {
super(props)
this.onVisible = this.onVisible.bind(this)
this.fetchReferrers = this.fetchReferrers.bind(this)
this.state = { loading: true }
}
onVisible() {
this.fetchReferrers()
if (this.props.query.period === 'realtime') {
document.addEventListener('tick', this.fetchReferrers)
}
}
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.setState({ loading: true, referrers: null })
this.fetchReferrers()
}
}
componentWillUnmount() {
document.removeEventListener('tick', this.fetchReferrers)
}
showConversionRate() {
return !!this.props.query.filters.goal
}
fetchReferrers() {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/sources`, this.props.query)
.then((res) => this.setState({ loading: false, referrers: res }))
}
renderReferrer(referrer) {
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
return (
<div
className="flex items-center justify-between my-1 text-sm"
key={referrer.name}
>
<Bar
count={referrer.visitors}
all={this.state.referrers}
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
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={url.setQuery('source', referrer.name)}
>
<img
src={`/favicon/sources/${encodeURIComponent(referrer.name)}`}
className="inline w-4 h-4 mr-2 -mt-px align-middle"
/>
{referrer.name}
</Link>
</span>
</Bar>
<span className="font-medium dark:text-gray-200 w-20 text-right" tooltip={referrer.visitors}>{numberFormatter(referrer.visitors)}</span>
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{referrer.conversion_rate}%</span>}
</div>
)
}
label() {
if (this.props.query.period === 'realtime') {
return 'Current visitors'
}
if (this.showConversionRate()) {
return 'Conversions'
}
return 'Visitors'
}
renderList() {
if (this.state.referrers && this.state.referrers.length > 0) {
return (
<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>
<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">
{this.state.referrers.map(this.renderReferrer.bind(this))}
</FlipMove>
<MoreLink site={this.props.site} list={this.state.referrers} endpoint="sources" className="pb-4 absolute bottom-0 left-0"/>
</React.Fragment>
)
} else {
return <div className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400">No data yet</div>
}
}
render() {
return (
<div className="flex flex-col flex-grow">
<LazyLoader onVisible={this.onVisible}>
<div id="sources" className="flex justify-between w-full">
<h3 className="font-bold dark:text-gray-100">Top Sources</h3>
{this.props.renderTabs()}
</div>
{this.state.loading && <div className="mx-auto loading mt-44"><div></div></div>}
<FadeIn show={!this.state.loading} className="flex flex-col flex-grow">
{this.renderList()}
</FadeIn>
</LazyLoader>
</div>
)
}
}
const UTM_TAGS = {
utm_medium: { label: 'UTM Medium', shortLabel: 'UTM Medium', endpoint: 'utm_mediums' },
utm_source: { label: 'UTM Source', shortLabel: 'UTM Source', endpoint: 'utm_sources' },
utm_campaign: { label: 'UTM Campaign', shortLabel: 'UTM Campai', endpoint: 'utm_campaigns' },
utm_content: { label: 'UTM Content', shortLabel: 'UTM Conten', endpoint: 'utm_contents' },
utm_term: { label: 'UTM Term', shortLabel: 'UTM Term', endpoint: 'utm_terms' },
}
class UTMSources extends React.Component {
constructor(props) {
super(props)
this.onVisible = this.onVisible.bind(this)
this.fetchReferrers = this.fetchReferrers.bind(this)
this.state = { loading: true }
}
onVisible() {
this.fetchReferrers()
if (this.props.query.period === 'realtime') {
document.addEventListener('tick', this.fetchReferrers)
}
}
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query || this.props.tab !== prevProps.tab) {
this.setState({ loading: true, referrers: null })
this.fetchReferrers()
}
}
componentWillUnmount() {
document.removeEventListener('tick', this.fetchReferrers)
}
showNoRef() {
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)
.then((res) => this.setState({ loading: false, referrers: res }))
}
renderReferrer(referrer) {
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
return (
<div
className="flex items-center justify-between my-1 text-sm"
key={referrer.name}
>
<Bar
count={referrer.visitors}
all={this.state.referrers}
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
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={url.setQuery(this.props.tab, referrer.name)}
>
{referrer.name}
</Link>
</span>
</Bar>
<span className="font-medium dark:text-gray-200 w-20 text-right" tooltip={referrer.visitors}>{numberFormatter(referrer.visitors)}</span>
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{referrer.conversion_rate}%</span>}
</div>
)
}
label() {
if (this.props.query.period === 'realtime') {
return 'Current visitors'
}
if (this.showConversionRate()) {
return 'Conversions'
}
return 'Visitors'
}
renderList() {
if (this.state.referrers && this.state.referrers.length > 0) {
return (
<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>
<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">
{this.state.referrers.map(this.renderReferrer.bind(this))}
</FlipMove>
<MoreLink site={this.props.site} list={this.state.referrers} endpoint={UTM_TAGS[this.props.tab].endpoint} className="pb-4 absolute bottom-0 left-0"/>
</div>
)
} else {
return <div className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400">No data yet</div>
}
}
render() {
return (
<div>
<LazyLoader onVisible={this.onVisible}>
<div className="flex justify-between w-full">
<h3 className="font-bold dark:text-gray-100">Top Sources</h3>
{this.props.renderTabs()}
</div>
{this.state.loading && <div className="mx-auto loading mt-44"><div></div></div>}
<FadeIn show={!this.state.loading} className="flex flex-col flex-grow">
{this.renderList()}
</FadeIn>
</LazyLoader>
</div>
)
}
}
import { Fragment } from 'react'
import * as api from '../../api'
import ListReport from '../reports/list'
import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics';
import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames'
export default class SourceList extends React.Component {
constructor(props) {
super(props)
this.tabKey = 'sourceTab__' + props.site.domain
const storedTab = storage.getItem(this.tabKey)
this.state = {
tab: storedTab || 'all'
}
const UTM_TAGS = {
utm_medium: { label: 'UTM Medium', shortLabel: 'UTM Medium', endpoint: '/utm_mediums' },
utm_source: { label: 'UTM Source', shortLabel: 'UTM Source', endpoint: '/utm_sources' },
utm_campaign: { label: 'UTM Campaign', shortLabel: 'UTM Campai', endpoint: '/utm_campaigns' },
utm_content: { label: 'UTM Content', shortLabel: 'UTM Conten', endpoint: '/utm_contents' },
utm_term: { label: 'UTM Term', shortLabel: 'UTM Term', endpoint: '/utm_terms' },
}
function AllSources(props) {
const {site, query} = props
function fetchData() {
return api.get(url.apiPath(site, '/sources'), query, {limit: 9})
}
setTab(tab) {
function getFilterFor(listItem) {
return { source: listItem['name']}
}
function renderIcon(listItem) {
return (
<img
src={`/favicon/sources/${encodeURIComponent(listItem.name)}`}
className="inline w-4 h-4 mr-2 -mt-px align-middle"
/>
)
}
return (
<ListReport
fetchData={fetchData}
getFilterFor={getFilterFor}
keyLabel="Source"
metrics={maybeWithCR([VISITORS_METRIC], query)}
detailsLink={url.sitePath(site, '/sources')}
renderIcon={renderIcon}
query={query}
color="bg-blue-50"
/>
)
}
function UTMSources(props) {
const {site, query} = props
const utmTag = UTM_TAGS[props.tab]
function fetchData() {
return api.get(url.apiPath(site, utmTag.endpoint), query, {limit: 9})
}
function getFilterFor(listItem) {
return { exit_page: listItem['name']}
}
return (
<ListReport
fetchData={fetchData}
getFilterFor={getFilterFor}
keyLabel={utmTag.label}
metrics={maybeWithCR([VISITORS_METRIC], query)}
detailsLink={url.sitePath(site, utmTag.endpoint)}
query={query}
color="bg-blue-50"
/>
)
}
export default function SourceList(props) {
const {site, query} = props
const tabKey = 'sourceTab__' + props.site.domain
const storedTab = storage.getItem(tabKey)
const [currentTab, setCurrentTab] = useState(storedTab || 'all')
function setTab(tab) {
return () => {
storage.setItem(this.tabKey, tab)
this.setState({ tab })
storage.setItem(tabKey, tab)
setCurrentTab(tab)
}
}
renderTabs() {
function renderTabs() {
const activeClass = 'inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold active-prop-heading truncate text-left'
const defaultClass = 'hover:text-indigo-600 cursor-pointer truncate text-left'
const dropdownOptions = Object.keys(UTM_TAGS)
let buttonText = UTM_TAGS[this.state.tab] ? UTM_TAGS[this.state.tab].label : 'Campaigns'
let buttonText = UTM_TAGS[currentTab] ? UTM_TAGS[currentTab].label : 'Campaigns'
return (
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
<div className={this.state.tab === 'all' ? activeClass : defaultClass} onClick={this.setTab('all')}>All</div>
<div className={currentTab === 'all' ? activeClass : defaultClass} onClick={setTab('all')}>All</div>
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="inline-flex justify-between focus:outline-none">
<span className={this.state.tab.startsWith('utm_') ? activeClass : defaultClass}>{buttonText}</span>
<span className={currentTab.startsWith('utm_') ? activeClass : defaultClass}>{buttonText}</span>
<ChevronDownIcon className="-mr-1 ml-1 h-4 w-4" aria-hidden="true" />
</Menu.Button>
</div>
@ -319,11 +123,11 @@ export default class SourceList extends React.Component {
<Menu.Item key={option}>
{({ active }) => (
<span
onClick={this.setTab(option)}
onClick={setTab(option)}
className={classNames(
active ? 'bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-200 cursor-pointer' : 'text-gray-700 dark:text-gray-200',
'block px-4 py-2 text-sm',
this.state.tab === option ? 'font-bold' : ''
currentTab === option ? 'font-bold' : ''
)}
>
{UTM_TAGS[option].label}
@ -340,11 +144,25 @@ export default class SourceList extends React.Component {
)
}
render() {
if (this.state.tab === 'all') {
return <AllSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
} else if (Object.keys(UTM_TAGS).includes(this.state.tab)) {
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
function renderContent() {
if (currentTab === 'all') {
return <AllSources site={site} query={query} />
} else {
return <UTMSources tab={currentTab} site={site} query={query} />
}
}
return (
<div>
{/* Header Container */}
<div className="w-full flex justify-between">
<h3 className="font-bold dark:text-gray-100">
Top Sources
</h3>
{ renderTabs() }
</div>
{/* Main Contents */}
{ renderContent() }
</div>
)
}

View File

@ -754,10 +754,8 @@ defmodule PlausibleWeb.Api.StatsController do
Stats.breakdown(site, query, "visit:referrer", metrics, pagination)
|> add_cr(site, query, pagination, :referrer, "visit:referrer")
|> transform_keys(%{referrer: :name})
|> Enum.map(&Map.drop(&1, [:visits]))
%{:visitors => %{value: total_visitors}} = Stats.aggregate(site, query, [:visitors])
json(conn, %{referrers: referrers, total_visitors: total_visitors})
json(conn, referrers)
end
def pages(conn, params) do

View File

@ -47,7 +47,8 @@ defmodule PlausibleWeb.Api.InternalControllerTest do
%{"data" => sites} = json_response(conn, 200)
assert Enum.map(sites, & &1["domain"]) == [site.domain, site2.domain]
assert %{"domain" => site.domain} in sites
assert %{"domain" => site2.domain} in sites
end
end

View File

@ -923,13 +923,10 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
"/api/stats/#{site.domain}/referrers/10words?period=day"
)
assert json_response(conn, 200) == %{
"total_visitors" => 3,
"referrers" => [
%{"name" => "10words.com", "visitors" => 2},
%{"name" => "10words.com/page1", "visitors" => 1}
]
}
assert json_response(conn, 200) == [
%{"name" => "10words.com", "visitors" => 2},
%{"name" => "10words.com/page1", "visitors" => 1}
]
end
test "calculates bounce rate and visit duration for referrer urls", %{conn: conn, site: site} do
@ -964,17 +961,14 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
"/api/stats/#{site.domain}/referrers/10words?period=day&date=2021-01-01&detailed=true"
)
assert json_response(conn, 200) == %{
"total_visitors" => 2,
"referrers" => [
%{
"name" => "10words.com",
"visitors" => 2,
"bounce_rate" => 50.0,
"visit_duration" => 450
}
]
}
assert json_response(conn, 200) == [
%{
"name" => "10words.com",
"visitors" => 2,
"bounce_rate" => 50.0,
"visit_duration" => 450
}
]
end
test "gets keywords from Google", %{conn: conn, user: user, site: site} do
@ -1032,17 +1026,14 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
"/api/stats/#{site.domain}/referrers/10words?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == %{
"total_visitors" => 1,
"referrers" => [
%{
"name" => "10words.com",
"total_visitors" => 2,
"conversion_rate" => 50.0,
"visitors" => 1
}
]
}
assert json_response(conn, 200) == [
%{
"name" => "10words.com",
"total_visitors" => 2,
"conversion_rate" => 50.0,
"visitors" => 1
}
]
end
test "returns top referring urls for a pageview goal", %{conn: conn, site: site} do
@ -1073,17 +1064,14 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
"/api/stats/#{site.domain}/referrers/10words?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == %{
"total_visitors" => 1,
"referrers" => [
%{
"name" => "10words.com",
"total_visitors" => 2,
"conversion_rate" => 50.0,
"visitors" => 1
}
]
}
assert json_response(conn, 200) == [
%{
"name" => "10words.com",
"total_visitors" => 2,
"conversion_rate" => 50.0,
"visitors" => 1
}
]
end
end