mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 17:44:43 +03:00
Show utm_medium, utm_source, and utm_campaign in sources modal (#321)
* Show utm_medium, utm_source, and utm_campaign in sources modal * Allow filtering by UTM tags * Integrate filters with URL bar * Add CHANGELOG entry * Refresh modal when filter changes * Remove Direct / None from campaign results * Add UTM tabs to top sources report * Add pagination * Remove dropdown from top sources popup * Fix bug in clickhouse_ecto * Remove referrers_for_goal * Make sure UTM tags work OK with goals * Make source tab selection sticky * Consistent styling for devices and source tabs * Add noref in realtime to utm tabs * Fix tests
This commit is contained in:
parent
10ac163aaa
commit
8d9667a949
@ -3,5 +3,8 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [1.0.0] - Unreleased
|
||||
|
||||
### Added
|
||||
- Collect and present link tags (`utm_medium`, `utm_source`, `utm_campaign`) in the dashboard
|
||||
|
||||
### Changed
|
||||
- Replace configuration parameters `CLICKHOUSE_DATABASE_{HOST,NAME,USER,PASSWORD}` with a single `CLICKHOUSE_DATABASE_URL` [plausible/analytics#317](https://github.com/plausible/analytics/pull/317)
|
||||
|
@ -9,6 +9,15 @@ function filterText(key, value) {
|
||||
if (key === "source") {
|
||||
return <span className="inline-block max-w-sm truncate">Source: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "utm_medium") {
|
||||
return <span className="inline-block max-w-sm truncate">UTM medium: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "utm_source") {
|
||||
return <span className="inline-block max-w-sm truncate">UTM source: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "utm_campaign") {
|
||||
return <span className="inline-block max-w-sm truncate">UTM campaign: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "referrer") {
|
||||
return <span className="inline-block max-w-sm truncate">Referrer: <b>{value}</b></span>
|
||||
}
|
||||
|
@ -25,6 +25,9 @@ export function parseQuery(querystring, site) {
|
||||
filters: {
|
||||
'goal': q.get('goal'),
|
||||
'source': q.get('source'),
|
||||
'utm_medium': q.get('utm_medium'),
|
||||
'utm_source': q.get('utm_source'),
|
||||
'utm_campaign': q.get('utm_campaign'),
|
||||
'referrer': q.get('referrer'),
|
||||
'page': q.get('page')
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Dash from './index'
|
||||
import Modal from './stats/modals/modal'
|
||||
import ReferrersModal from './stats/modals/referrers'
|
||||
import SourcesModal from './stats/modals/sources'
|
||||
import ReferrersDrilldownModal from './stats/modals/referrer-drilldown'
|
||||
import GoogleKeywordsModal from './stats/modals/google-keywords'
|
||||
import PagesModal from './stats/modals/pages'
|
||||
@ -28,8 +27,8 @@ export default function Router({site, loggedIn}) {
|
||||
<ScrollToTop />
|
||||
<Dash site={site} loggedIn={loggedIn} />
|
||||
<Switch>
|
||||
<Route exact path="/:domain/referrers">
|
||||
<ReferrersModal site={site} />
|
||||
<Route exact path={["/:domain/sources", "/:domain/utm_mediums", "/:domain/utm_sources", "/:domain/utm_campaigns"]}>
|
||||
<SourcesModal site={site} />
|
||||
</Route>
|
||||
<Route exact path="/:domain/referrers/Google">
|
||||
<GoogleKeywordsModal site={site} />
|
||||
|
@ -242,7 +242,11 @@ class OperatingSystems extends React.Component {
|
||||
export default class Devices extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {mode: 'size'}
|
||||
this.tabKey = 'deviceTab__' + props.site.domain
|
||||
const storedTab = window.localStorage[this.tabKey]
|
||||
this.state = {
|
||||
mode: storedTab || 'size'
|
||||
}
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
@ -257,6 +261,7 @@ export default class Devices extends React.Component {
|
||||
|
||||
setMode(mode) {
|
||||
return () => {
|
||||
window.localStorage[this.tabKey] = mode
|
||||
this.setState({mode})
|
||||
}
|
||||
}
|
||||
@ -266,9 +271,9 @@ export default class Devices extends React.Component {
|
||||
const extraClass = name === 'OS' ? '' : ' border-r border-gray-300'
|
||||
|
||||
if (isActive) {
|
||||
return <span className={"inline-block shadow-inner text-sm font-bold py-1 px-4" + extraClass}>{name}</span>
|
||||
return <li className="inline-block h-5 text-indigo-700 font-bold border-b-2 border-indigo-700" onClick={this.setMode(mode)}>{name}</li>
|
||||
} else {
|
||||
return <span className={"inline-block cursor-pointer bg-gray-100 text-sm font-bold py-1 px-4" + extraClass} onClick={this.setMode(mode)}>{name}</span>
|
||||
return <li className="hover:text-indigo-700 cursor-pointer" onClick={this.setMode(mode)}>{name}</li>
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,12 +281,15 @@ export default class Devices extends React.Component {
|
||||
return (
|
||||
<div className="stats-item">
|
||||
<div className="bg-white shadow-xl rounded p-4 relative" style={{height: '436px'}}>
|
||||
<h3 className="font-bold">Devices</h3>
|
||||
|
||||
<div className="rounded border border-gray-300 absolute" style={{top: '1rem', right: '1rem'}}>
|
||||
{ this.renderPill('Size', 'size') }
|
||||
{ this.renderPill('Browser', 'browser') }
|
||||
{ this.renderPill('OS', 'os') }
|
||||
<div className="w-full flex justify-between">
|
||||
<h3 className="font-bold">Devices</h3>
|
||||
|
||||
<ul className="flex font-medium text-xs text-gray-500 space-x-2">
|
||||
{ this.renderPill('Size', 'size') }
|
||||
{ this.renderPill('Browser', 'browser') }
|
||||
{ this.renderPill('OS', 'os') }
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{ this.renderContent() }
|
||||
|
@ -1,40 +1,54 @@
|
||||
import React from "react";
|
||||
import { Link, withRouter } from 'react-router-dom'
|
||||
|
||||
import Transition from "../../../transition.js";
|
||||
import FadeIn from '../../fade-in'
|
||||
import Modal from './modal'
|
||||
import * as api from '../../api'
|
||||
import numberFormatter, {durationFormatter} from '../../number-formatter'
|
||||
import {parseQuery} from '../../query'
|
||||
|
||||
class ReferrersModal extends React.Component {
|
||||
const TITLES = {
|
||||
sources: 'Top sources',
|
||||
utm_mediums: 'Top UTM mediums',
|
||||
utm_sources: 'Top UTM sources',
|
||||
utm_campaigns: 'Top UTM campaigns'
|
||||
}
|
||||
|
||||
class SourcesModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
loading: true,
|
||||
referrers: [],
|
||||
sources: [],
|
||||
query: parseQuery(props.location.search, props.site),
|
||||
page: 1,
|
||||
moreResultsAvailable: false
|
||||
}
|
||||
}
|
||||
|
||||
loadReferrers() {
|
||||
const {query, page, referrers} = this.state
|
||||
loadSources() {
|
||||
const {site} = this.props
|
||||
const {query, page, sources} = this.state
|
||||
|
||||
if (query.filters.goal) {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/goal/referrers`, query, {limit: 100, page: page})
|
||||
.then((res) => this.setState({loading: false, referrers: referrers.concat(res), moreResultsAvailable: res.length === 100}))
|
||||
} else {
|
||||
const include = this.showExtra() ? 'bounce_rate,visit_duration' : null
|
||||
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers`, query, {limit: 100, page: page, include: include, show_noref: true})
|
||||
.then((res) => this.setState({loading: false, referrers: referrers.concat(res), moreResultsAvailable: res.length === 100}))
|
||||
}
|
||||
const include = this.showExtra() ? 'bounce_rate,visit_duration' : null
|
||||
api.get(`/api/stats/${encodeURIComponent(site.domain)}/${this.currentFilter()}`, query, {limit: 100, page: page, include: include, show_noref: true})
|
||||
.then((res) => this.setState({loading: false, sources: sources.concat(res), moreResultsAvailable: res.length === 100}))
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadReferrers()
|
||||
this.loadSources()
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.location.pathname !== prevProps.location.pathname) {
|
||||
this.setState({sources: [], loading: true}, this.loadSources.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
currentFilter() {
|
||||
const urlparts = this.props.location.pathname.split('/')
|
||||
return urlparts[urlparts.length - 1]
|
||||
}
|
||||
|
||||
showExtra() {
|
||||
@ -42,7 +56,7 @@ class ReferrersModal extends React.Component {
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.setState({loading: true, page: this.state.page + 1}, this.loadReferrers.bind(this))
|
||||
this.setState({loading: true, page: this.state.page + 1}, this.loadSources.bind(this))
|
||||
}
|
||||
|
||||
formatBounceRate(page) {
|
||||
@ -53,27 +67,31 @@ class ReferrersModal extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
formatDuration(referrer) {
|
||||
if (typeof(referrer.visit_duration) === 'number') {
|
||||
return durationFormatter(referrer.visit_duration)
|
||||
formatDuration(source) {
|
||||
if (typeof(source.visit_duration) === 'number') {
|
||||
return durationFormatter(source.visit_duration)
|
||||
} else {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
renderReferrer(referrer) {
|
||||
renderSource(source) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('source', referrer.name)
|
||||
const filter = this.currentFilter()
|
||||
if (filter === 'sources') query.set('source', source.name)
|
||||
if (filter === 'utm_mediums') query.set('utm_medium', source.name)
|
||||
if (filter === 'utm_sources') query.set('utm_source', source.name)
|
||||
if (filter === 'utm_campaigns') query.set('utm_campaign', source.name)
|
||||
|
||||
return (
|
||||
<tr className="text-sm" key={referrer.name}>
|
||||
<tr className="text-sm" key={source.name}>
|
||||
<td className="p-2">
|
||||
<img src={`https://icons.duckduckgo.com/ip3/${referrer.url}.ico`} className="h-4 w-4 mr-2 align-middle inline" />
|
||||
<Link className="hover:underline truncate" style={{maxWidth: '80%'}} to={{search: query.toString(), pathname: '/' + encodeURIComponent(this.props.site.domain)}}>{ referrer.name }</Link>
|
||||
<img src={`https://icons.duckduckgo.com/ip3/${source.url}.ico`} className="h-4 w-4 mr-2 align-middle inline" />
|
||||
<Link className="hover:underline" to={{search: query.toString(), pathname: '/' + encodeURIComponent(this.props.site.domain)}}>{ source.name }</Link>
|
||||
</td>
|
||||
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(referrer.count)}</td>
|
||||
{this.showExtra() && <td className="p-2 w-32 font-medium" align="right">{this.formatBounceRate(referrer)}</td> }
|
||||
{this.showExtra() && <td className="p-2 w-32 font-medium" align="right">{this.formatDuration(referrer)}</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.formatDuration(source)}</td> }
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@ -96,10 +114,14 @@ class ReferrersModal extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
title() {
|
||||
return TITLES[this.currentFilter()]
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal site={this.props.site}>
|
||||
<h1 className="text-xl font-bold">Top Sources</h1>
|
||||
<h1 className="text-xl font-bold">{this.title()}</h1>
|
||||
|
||||
<div className="my-4 border-b border-gray-300"></div>
|
||||
|
||||
@ -107,14 +129,14 @@ class ReferrersModal extends React.Component {
|
||||
<table className="w-full table-striped table-fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2 text-xs tracking-wide font-bold text-gray-500" align="left">Referrer</th>
|
||||
<th className="p-2 text-xs tracking-wide font-bold text-gray-500" align="left">Source</th>
|
||||
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500" align="right">{this.label()}</th>
|
||||
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500" align="right">Bounce rate</th>}
|
||||
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500" align="right">Visit duration</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ this.state.referrers.map(this.renderReferrer.bind(this)) }
|
||||
{ this.state.sources.map(this.renderSource.bind(this)) }
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
@ -125,4 +147,4 @@ class ReferrersModal extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(ReferrersModal)
|
||||
export default withRouter(SourcesModal)
|
@ -8,7 +8,7 @@ import MoreLink from '../more-link'
|
||||
import numberFormatter from '../../number-formatter'
|
||||
import * as api from '../../api'
|
||||
|
||||
export default class Referrers extends React.Component {
|
||||
class AllSources extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {loading: true}
|
||||
@ -31,13 +31,8 @@ export default class Referrers extends React.Component {
|
||||
}
|
||||
|
||||
fetchReferrers() {
|
||||
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, {show_noref: this.showNoRef()})
|
||||
.then((res) => this.setState({loading: false, referrers: res}))
|
||||
}
|
||||
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}))
|
||||
}
|
||||
|
||||
renderReferrer(referrer) {
|
||||
@ -87,9 +82,12 @@ export default class Referrers extends React.Component {
|
||||
if (this.state.referrers) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h3 className="font-bold">Top sources</h3>
|
||||
<div className="w-full flex justify-between">
|
||||
<h3 className="font-bold">Top sources</h3>
|
||||
{ this.props.renderTabs() }
|
||||
</div>
|
||||
{ this.renderList() }
|
||||
<MoreLink site={this.props.site} list={this.state.referrers} endpoint="referrers" />
|
||||
<MoreLink site={this.props.site} list={this.state.referrers} endpoint="sources" />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@ -106,3 +104,149 @@ export default class Referrers extends React.Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const UTM_TAGS = {
|
||||
utm_medium: {label: 'UTM Medium', endpoint: 'utm_mediums'},
|
||||
utm_source: {label: 'UTM Source', endpoint: 'utm_sources'},
|
||||
utm_campaign: {label: 'UTM Campaign', endpoint: 'utm_campaigns'},
|
||||
}
|
||||
|
||||
class UTMSources extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {loading: true}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchReferrers()
|
||||
if (this.props.timer) this.props.timer.onTick(this.fetchReferrers.bind(this))
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.query !== prevProps.query || this.props.tab !== prevProps.tab) {
|
||||
this.setState({loading: true, referrers: null})
|
||||
this.fetchReferrers()
|
||||
}
|
||||
}
|
||||
|
||||
showNoRef() {
|
||||
return this.props.query.period === 'realtime'
|
||||
}
|
||||
|
||||
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()})
|
||||
.then((res) => this.setState({loading: false, referrers: res}))
|
||||
}
|
||||
|
||||
renderReferrer(referrer) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set(this.props.tab, referrer.name)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={referrer.name}>
|
||||
<div className="w-full h-8" style={{maxWidth: 'calc(100% - 4rem)'}}>
|
||||
<Bar count={referrer.count} all={this.state.referrers} bg="bg-blue-50" />
|
||||
<span className="flex px-2" style={{marginTop: '-26px'}} >
|
||||
<Link className="block truncate hover:underline" to={{search: query.toString()}}>
|
||||
{ referrer.name }
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium">{numberFormatter(referrer.count)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
label() {
|
||||
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||
}
|
||||
|
||||
renderList() {
|
||||
if (this.state.referrers.length > 0) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="flex items-center mt-3 mb-2 justify-between text-gray-500 text-xs font-bold tracking-wide">
|
||||
<span>{UTM_TAGS[this.props.tab].label}</span>
|
||||
<span>{this.label()}</span>
|
||||
</div>
|
||||
|
||||
<FlipMove>
|
||||
{this.state.referrers.map(this.renderReferrer.bind(this))}
|
||||
</FlipMove>
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return <div className="text-center mt-44 font-medium text-gray-500">No data yet</div>
|
||||
}
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (this.state.referrers) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="w-full flex justify-between">
|
||||
<h3 className="font-bold">Top sources</h3>
|
||||
{ this.props.renderTabs() }
|
||||
</div>
|
||||
{ this.renderList() }
|
||||
<MoreLink site={this.props.site} list={this.state.referrers} endpoint={UTM_TAGS[this.props.tab].endpoint} />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="stats-item relative bg-white shadow-xl rounded p-4" style={{height: '436px'}}>
|
||||
{ this.state.loading && <div className="loading mt-44 mx-auto"><div></div></div> }
|
||||
<FadeIn show={!this.state.loading}>
|
||||
{ this.renderContent() }
|
||||
</FadeIn>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default class SourceList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.tabKey = 'sourceTab__' + props.site.domain
|
||||
const storedTab = window.localStorage[this.tabKey]
|
||||
this.state = {
|
||||
tab: storedTab || 'all'
|
||||
}
|
||||
}
|
||||
|
||||
setTab(tab) {
|
||||
return () => {
|
||||
window.localStorage[this.tabKey] = tab
|
||||
this.setState({tab})
|
||||
}
|
||||
}
|
||||
|
||||
renderTabs() {
|
||||
const activeClass = 'inline-block h-5 text-indigo-700 font-bold border-b-2 border-indigo-700'
|
||||
const defaultClass = 'hover:text-indigo-700 cursor-pointer'
|
||||
return (
|
||||
<ul className="flex font-medium text-xs text-gray-500 space-x-2">
|
||||
<li className={this.state.tab === 'all' ? activeClass : defaultClass} onClick={this.setTab('all')}>All</li>
|
||||
<li className={this.state.tab === 'utm_medium' ? activeClass : defaultClass} onClick={this.setTab('utm_medium')}>Medium</li>
|
||||
<li className={this.state.tab === 'utm_source' ? activeClass : defaultClass} onClick={this.setTab('utm_source')}>Source</li>
|
||||
<li className={this.state.tab === 'utm_campaign' ? activeClass : defaultClass} onClick={this.setTab('utm_campaign')}>Campaign</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
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 (this.state.tab === 'utm_medium') {
|
||||
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
||||
} else if (this.state.tab === 'utm_source') {
|
||||
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
||||
} else if (this.state.tab === 'utm_campaign') {
|
||||
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
end)
|
||||
end
|
||||
|
||||
def top_referrers(site, query, limit, page, show_noref \\ false, include \\ []) do
|
||||
def top_sources(site, query, limit, page, show_noref \\ false, include \\ []) do
|
||||
offset = (page - 1) * limit
|
||||
|
||||
referrers =
|
||||
@ -203,7 +203,7 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
order_by: [desc: fragment("count"), asc: fragment("min(start)")],
|
||||
limit: ^limit,
|
||||
offset: ^offset
|
||||
)
|
||||
) |> filter_converted_sessions(site, query)
|
||||
|
||||
referrers = if show_noref do
|
||||
referrers
|
||||
@ -247,6 +247,102 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
end)
|
||||
end
|
||||
|
||||
defp filter_converted_sessions(db_query, site, %Query{filters: %{"goal" => goal}} = query) when is_binary(goal) do
|
||||
converted_sessions =
|
||||
from(e in base_query(site, query),
|
||||
select: %{session_id: e.session_id})
|
||||
|
||||
from(s in db_query,
|
||||
join: cs in subquery(converted_sessions),
|
||||
on: s.session_id == cs.session_id
|
||||
)
|
||||
end
|
||||
defp filter_converted_sessions(db_query, _site, _query), do: db_query
|
||||
|
||||
def utm_mediums(site, query, limit \\ 9, page \\ 1, show_noref \\ false) do
|
||||
offset = (page - 1) * limit
|
||||
|
||||
q = from(
|
||||
s in base_session_query(site, query),
|
||||
group_by: s.utm_medium,
|
||||
order_by: [desc: fragment("count"), asc: fragment("min(start)")],
|
||||
limit: ^limit,
|
||||
offset: ^offset,
|
||||
select: %{
|
||||
name: fragment("if(empty(?), ?, ?) as name", s.utm_medium, @no_ref, s.utm_medium),
|
||||
count: fragment("uniq(user_id) as count"),
|
||||
bounce_rate: fragment("round(sum(is_bounce * sign) / sum(sign) * 100)"),
|
||||
visit_duration: fragment("round(avg(duration * sign))")
|
||||
}
|
||||
)
|
||||
|
||||
q = if show_noref do
|
||||
q
|
||||
else
|
||||
from(s in q, where: s.utm_medium != "")
|
||||
end
|
||||
|
||||
q
|
||||
|> filter_converted_sessions(site, query)
|
||||
|> ClickhouseRepo.all()
|
||||
end
|
||||
|
||||
def utm_campaigns(site, query, limit \\ 9, page \\ 1, show_noref \\ false) do
|
||||
offset = (page - 1) * limit
|
||||
|
||||
q = from(
|
||||
s in base_session_query(site, query),
|
||||
group_by: s.utm_campaign,
|
||||
order_by: [desc: fragment("count"), asc: fragment("min(start)")],
|
||||
limit: ^limit,
|
||||
offset: ^offset,
|
||||
select: %{
|
||||
name: fragment("if(empty(?), ?, ?) as name", s.utm_campaign, @no_ref, s.utm_campaign),
|
||||
count: fragment("uniq(user_id) as count"),
|
||||
bounce_rate: fragment("round(sum(is_bounce * sign) / sum(sign) * 100)"),
|
||||
visit_duration: fragment("round(avg(duration * sign))")
|
||||
}
|
||||
)
|
||||
|
||||
q = if show_noref do
|
||||
q
|
||||
else
|
||||
from(s in q, where: s.utm_campaign != "")
|
||||
end
|
||||
|
||||
q
|
||||
|> filter_converted_sessions(site, query)
|
||||
|> ClickhouseRepo.all()
|
||||
end
|
||||
|
||||
def utm_sources(site, query, limit \\ 9, page \\ 1, show_noref \\ false) do
|
||||
offset = (page - 1) * limit
|
||||
|
||||
q = from(
|
||||
s in base_session_query(site, query),
|
||||
group_by: s.utm_source,
|
||||
order_by: [desc: fragment("count"), asc: fragment("min(start)")],
|
||||
limit: ^limit,
|
||||
offset: ^offset,
|
||||
select: %{
|
||||
name: fragment("if(empty(?), ?, ?) as name", s.utm_source, @no_ref, s.utm_source),
|
||||
count: fragment("uniq(user_id) as count"),
|
||||
bounce_rate: fragment("round(sum(is_bounce * sign) / sum(sign) * 100)"),
|
||||
visit_duration: fragment("round(avg(duration * sign))")
|
||||
}
|
||||
)
|
||||
|
||||
q = if show_noref do
|
||||
q
|
||||
else
|
||||
from(s in q, where: s.utm_source != "")
|
||||
end
|
||||
|
||||
q
|
||||
|> filter_converted_sessions(site, query)
|
||||
|> ClickhouseRepo.all()
|
||||
end
|
||||
|
||||
def conversions_from_referrer(site, query, referrer) do
|
||||
converted_sessions =
|
||||
from(
|
||||
@ -543,43 +639,16 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
if Enum.count(events) > 0 do
|
||||
{first_datetime, last_datetime} = utc_boundaries(query, site.timezone)
|
||||
|
||||
q =
|
||||
from(
|
||||
e in "events",
|
||||
where: e.domain == ^site.domain,
|
||||
where: e.timestamp >= ^first_datetime and e.timestamp < ^last_datetime,
|
||||
where: fragment("? IN tuple(?)", e.name, ^events),
|
||||
group_by: e.name,
|
||||
select: %{
|
||||
name: e.name,
|
||||
count: fragment("uniq(user_id) as count"),
|
||||
total_count: fragment("count(*) as total_count")
|
||||
}
|
||||
)
|
||||
|
||||
q =
|
||||
if query.filters["source"] do
|
||||
filtered_sessions =
|
||||
from(s in base_session_query(site, query), select: %{session_id: s.session_id})
|
||||
|
||||
from(
|
||||
e in q,
|
||||
join: cs in subquery(filtered_sessions),
|
||||
on: e.session_id == cs.session_id
|
||||
)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["page"] do
|
||||
page = query.filters["page"]
|
||||
from(e in q, where: e.pathname == ^page)
|
||||
else
|
||||
q
|
||||
end
|
||||
q = from(
|
||||
e in base_query_w_sessions(site, query),
|
||||
where: fragment("? IN tuple(?)", e.name, ^events),
|
||||
group_by: e.name,
|
||||
select: %{
|
||||
name: e.name,
|
||||
count: fragment("uniq(user_id) as count"),
|
||||
total_count: fragment("count(*) as total_count")
|
||||
}
|
||||
)
|
||||
|
||||
ClickhouseRepo.all(q)
|
||||
else
|
||||
@ -593,43 +662,16 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
if Enum.count(pages) > 0 do
|
||||
{first_datetime, last_datetime} = utc_boundaries(query, site.timezone)
|
||||
|
||||
q =
|
||||
from(
|
||||
e in "events",
|
||||
where: e.domain == ^site.domain,
|
||||
where: e.timestamp >= ^first_datetime and e.timestamp < ^last_datetime,
|
||||
where: fragment("? IN tuple(?)", e.pathname, ^pages),
|
||||
group_by: e.pathname,
|
||||
select: %{
|
||||
name: fragment("concat('Visit ', ?) as name", e.pathname),
|
||||
count: fragment("uniq(user_id) as count"),
|
||||
total_count: fragment("count(*) as total_count")
|
||||
}
|
||||
)
|
||||
|
||||
q =
|
||||
if query.filters["source"] do
|
||||
filtered_sessions =
|
||||
from(s in base_session_query(site, query), select: %{session_id: s.session_id})
|
||||
|
||||
from(
|
||||
e in q,
|
||||
join: cs in subquery(filtered_sessions),
|
||||
on: e.session_id == cs.session_id
|
||||
)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["page"] do
|
||||
page = query.filters["page"]
|
||||
from(e in q, where: e.pathname == ^page)
|
||||
else
|
||||
q
|
||||
end
|
||||
q = from(
|
||||
e in base_query_w_sessions(site, query),
|
||||
where: fragment("? IN tuple(?)", e.pathname, ^pages),
|
||||
group_by: e.pathname,
|
||||
select: %{
|
||||
name: fragment("concat('Visit ', ?) as name", e.pathname),
|
||||
count: fragment("uniq(user_id) as count"),
|
||||
total_count: fragment("count(*) as total_count")
|
||||
}
|
||||
)
|
||||
|
||||
ClickhouseRepo.all(q)
|
||||
else
|
||||
@ -659,6 +701,30 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q =
|
||||
if query.filters["utm_medium"] do
|
||||
utm_medium = query.filters["utm_medium"]
|
||||
from(s in sessions_q, where: s.utm_medium == ^utm_medium)
|
||||
else
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q =
|
||||
if query.filters["utm_source"] do
|
||||
utm_source = query.filters["utm_source"]
|
||||
from(s in sessions_q, where: s.utm_source == ^utm_source)
|
||||
else
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q =
|
||||
if query.filters["utm_campaign"] do
|
||||
utm_campaign = query.filters["utm_campaign"]
|
||||
from(s in sessions_q, where: s.utm_campaign == ^utm_campaign)
|
||||
else
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q = if query.filters["referrer"] do
|
||||
ref = query.filters["referrer"]
|
||||
from(s in sessions_q, where: s.referrer == ^ref)
|
||||
@ -672,7 +738,7 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
where: e.timestamp >= ^first_datetime and e.timestamp < ^last_datetime
|
||||
)
|
||||
|
||||
q = if query.filters["source"] || query.filters['referrer'] do
|
||||
q = if query.filters["source"] || query.filters['referrer'] || query.filters["utm_medium"] || query.filters["utm_source"] || query.filters["utm_campaign"] do
|
||||
from(
|
||||
e in q,
|
||||
join: sq in subquery(sessions_q),
|
||||
@ -708,6 +774,30 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["utm_medium"] do
|
||||
utm_medium = query.filters["utm_medium"]
|
||||
from(s in q, where: s.utm_medium == ^utm_medium)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["utm_source"] do
|
||||
utm_source = query.filters["utm_source"]
|
||||
from(s in q, where: s.utm_source == ^utm_source)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["utm_campaign"] do
|
||||
utm_campaign = query.filters["utm_campaign"]
|
||||
from(s in q, where: s.utm_campaign == ^utm_campaign)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["page"] do
|
||||
page = query.filters["page"]
|
||||
@ -718,7 +808,7 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
|
||||
if query.filters["referrer"] do
|
||||
ref = query.filters["referrer"]
|
||||
from(e in q, where: e.referrer == ^ref)
|
||||
from(s in q, where: s.referrer == ^ref)
|
||||
else
|
||||
q
|
||||
end
|
||||
@ -743,6 +833,30 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["utm_medium"] do
|
||||
utm_medium = query.filters["utm_medium"]
|
||||
from(e in q, where: e.utm_medium == ^utm_medium)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["utm_source"] do
|
||||
utm_source = query.filters["utm_source"]
|
||||
from(e in q, where: e.utm_source == ^utm_source)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["utm_campaign"] do
|
||||
utm_campaign = query.filters["utm_campaign"]
|
||||
from(e in q, where: e.utm_campaign == ^utm_campaign)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["referrer"] do
|
||||
ref = query.filters["referrer"]
|
||||
|
@ -125,23 +125,41 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
end
|
||||
end
|
||||
|
||||
def referrers(conn, params) do
|
||||
def sources(conn, params) do
|
||||
site = conn.assigns[:site]
|
||||
query = Query.from(site.timezone, params)
|
||||
include = if params["include"], do: String.split(params["include"], ","), else: []
|
||||
limit = if params["limit"], do: String.to_integer(params["limit"])
|
||||
page = if params["page"], do: String.to_integer(params["page"])
|
||||
show_noref = params["show_noref"] == "true"
|
||||
json(conn, Stats.top_referrers(site, query, limit || 9, page || 1, show_noref, include))
|
||||
json(conn, Stats.top_sources(site, query, limit || 9, page || 1, show_noref, include))
|
||||
end
|
||||
|
||||
def referrers_for_goal(conn, params) do
|
||||
def utm_mediums(conn, params) do
|
||||
site = conn.assigns[:site]
|
||||
query = Query.from(site.timezone, params)
|
||||
limit = if params["limit"], do: String.to_integer(params["limit"])
|
||||
page = if params["page"], do: String.to_integer(params["page"])
|
||||
show_noref = params["show_noref"] == "true"
|
||||
json(conn, Stats.utm_mediums(site, query, limit || 9, page || 1, show_noref))
|
||||
end
|
||||
|
||||
json(conn, Stats.top_referrers_for_goal(site, query, limit || 9, page || 1))
|
||||
def utm_campaigns(conn, params) do
|
||||
site = conn.assigns[:site]
|
||||
query = Query.from(site.timezone, params)
|
||||
limit = if params["limit"], do: String.to_integer(params["limit"])
|
||||
page = if params["page"], do: String.to_integer(params["page"])
|
||||
show_noref = params["show_noref"] == "true"
|
||||
json(conn, Stats.utm_campaigns(site, query, limit || 9, page || 1, show_noref))
|
||||
end
|
||||
|
||||
def utm_sources(conn, params) do
|
||||
site = conn.assigns[:site]
|
||||
query = Query.from(site.timezone, params)
|
||||
limit = if params["limit"], do: String.to_integer(params["limit"])
|
||||
page = if params["page"], do: String.to_integer(params["page"])
|
||||
show_noref = params["show_noref"] == "true"
|
||||
json(conn, Stats.utm_sources(site, query, limit || 9, page || 1, show_noref))
|
||||
end
|
||||
|
||||
@google_api Application.fetch_env!(:plausible, :google_api)
|
||||
|
@ -46,8 +46,10 @@ defmodule PlausibleWeb.Router do
|
||||
|
||||
get "/:domain/current-visitors", StatsController, :current_visitors
|
||||
get "/:domain/main-graph", StatsController, :main_graph
|
||||
get "/:domain/referrers", StatsController, :referrers
|
||||
get "/:domain/goal/referrers", StatsController, :referrers_for_goal
|
||||
get "/:domain/sources", StatsController, :sources
|
||||
get "/:domain/utm_mediums", StatsController, :utm_mediums
|
||||
get "/:domain/utm_sources", StatsController, :utm_sources
|
||||
get "/:domain/utm_campaigns", StatsController, :utm_campaigns
|
||||
get "/:domain/referrers/:referrer", StatsController, :referrer_drilldown
|
||||
get "/:domain/goal/referrers/:referrer", StatsController, :referrer_drilldown_for_goal
|
||||
get "/:domain/pages", StatsController, :pages
|
||||
|
@ -55,7 +55,7 @@ defmodule Plausible.Workers.SendEmailReport do
|
||||
bounce_rate = Stats.bounce_rate(site, query)
|
||||
prev_bounce_rate = Stats.bounce_rate(site, Query.shift_back(query))
|
||||
change_bounce_rate = if prev_bounce_rate > 0, do: bounce_rate - prev_bounce_rate
|
||||
referrers = Stats.top_referrers(site, query, 5, 1, [])
|
||||
referrers = Stats.top_sources(site, query, 5, 1, [])
|
||||
pages = Stats.top_pages(site, query, 5, [])
|
||||
user = Plausible.Auth.find_user_by(email: email)
|
||||
login_link = user && Plausible.Sites.is_owner?(user.id, site)
|
||||
|
2
mix.lock
2
mix.lock
@ -5,7 +5,7 @@
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.2.0", "3df902b81ce7fa8867a2ae30d20a1da6877a2c056bfb116fd0bc8a5f0190cea4", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "762be3fcb779f08207531bc6612cca480a338e4b4357abb49f5ce00240a77d1e"},
|
||||
"browser": {:hex, :browser, "0.4.4", "bd6436961a6b2299c6cb38d0e49761c1161d869cd0db46369cef2bf6b77c3665", [:mix], [{:plug, "~> 1.2", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d476ca309d4a4b19742b870380390aabbcb323c1f6f8745e2da2dfd079b4f8d7"},
|
||||
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
|
||||
"clickhouse_ecto": {:git, "https://github.com/plausible/clickhouse_ecto.git", "bd644593820c6e9c19f25e2cedc8891c5a57f60f", []},
|
||||
"clickhouse_ecto": {:git, "https://github.com/plausible/clickhouse_ecto.git", "2d79f932bdb7f8894668c650b9dcc124d2145aeb", []},
|
||||
"clickhousex": {:git, "https://github.com/plausible/clickhousex", "89d58d4cb0cad2558e874f30e81a5c2c84ada95e", []},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
|
||||
|
@ -1,12 +1,12 @@
|
||||
defmodule PlausibleWeb.Api.StatsController.ReferrersTest do
|
||||
defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
use PlausibleWeb.ConnCase
|
||||
import Plausible.TestUtils
|
||||
|
||||
describe "GET /api/stats/:domain/referrers" do
|
||||
describe "GET /api/stats/:domain/sources" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "returns top referrer sources by user ids", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/referrers?period=day&date=2019-01-01")
|
||||
test "returns top sources by unique user ids", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources?period=day&date=2019-01-01")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "10words", "count" => 2, "url" => "10words.com"},
|
||||
@ -14,11 +14,11 @@ defmodule PlausibleWeb.Api.StatsController.ReferrersTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "calculates bounce rate and visit duration for referrers", %{conn: conn, site: site} do
|
||||
test "calculates bounce rate and visit duration for sources", %{conn: conn, site: site} do
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/referrers?period=day&date=2019-01-01&include=bounce_rate,visit_duration"
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2019-01-01&include=bounce_rate,visit_duration"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
@ -39,8 +39,8 @@ defmodule PlausibleWeb.Api.StatsController.ReferrersTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "returns top referrer sources in realtime report", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/referrers?period=realtime")
|
||||
test "returns top sources in realtime report", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources?period=realtime")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "10words", "count" => 2, "url" => "10words.com"},
|
||||
@ -49,7 +49,7 @@ defmodule PlausibleWeb.Api.StatsController.ReferrersTest do
|
||||
end
|
||||
|
||||
test "can paginate the results", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/referrers?period=day&date=2019-01-01&limit=1&page=2")
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources?period=day&date=2019-01-01&limit=1&page=2")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "Bing", "count" => 1, "url" => ""}
|
||||
@ -57,6 +57,18 @@ defmodule PlausibleWeb.Api.StatsController.ReferrersTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/utm_mediums" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "returns top utm_mediums by unique user ids", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/utm_mediums?period=day&date=2019-01-01")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "listing", "count" => 2, "bounce_rate" => 50.0, "visit_duration" => 50.0},
|
||||
%{"name" => "search", "count" => 1, "bounce_rate" => 0.0, "visit_duration" => 100.0}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/goal/referrers" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
@ -67,7 +79,7 @@ defmodule PlausibleWeb.Api.StatsController.ReferrersTest do
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/goal/referrers?period=day&date=2019-01-01&filters=#{filters}"
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2019-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
@ -81,7 +93,7 @@ defmodule PlausibleWeb.Api.StatsController.ReferrersTest do
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/goal/referrers?period=day&date=2019-01-01&filters=#{filters}"
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2019-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
@ -98,7 +110,7 @@ defmodule PlausibleWeb.Api.StatsController.ReferrersTest do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/referrers/10words?period=day&date=2019-01-01&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"total_visitors" => 2,
|
||||
"total_visitors" => 6,
|
||||
"referrers" => [
|
||||
%{"name" => "10words.com/page1", "url" => "10words.com", "count" => 2}
|
||||
]
|
||||
@ -114,7 +126,7 @@ defmodule PlausibleWeb.Api.StatsController.ReferrersTest do
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"total_visitors" => 2,
|
||||
"total_visitors" => 6,
|
||||
"referrers" => [
|
||||
%{
|
||||
"name" => "10words.com/page1",
|
@ -147,6 +147,7 @@ defmodule Plausible.Test.ClickhouseSetup do
|
||||
exit_page: "/",
|
||||
referrer_source: "10words",
|
||||
referrer: "10words.com/page1",
|
||||
utm_medium: "listing",
|
||||
session_id: @conversion_1_session_id,
|
||||
is_bounce: true,
|
||||
duration: 100,
|
||||
@ -159,6 +160,7 @@ defmodule Plausible.Test.ClickhouseSetup do
|
||||
exit_page: "/",
|
||||
referrer_source: "10words",
|
||||
referrer: "10words.com/page1",
|
||||
utm_medium: "listing",
|
||||
session_id: @conversion_2_session_id,
|
||||
is_bounce: false,
|
||||
duration: 0,
|
||||
@ -171,6 +173,7 @@ defmodule Plausible.Test.ClickhouseSetup do
|
||||
exit_page: "/",
|
||||
referrer_source: "Bing",
|
||||
referrer: "",
|
||||
utm_medium: "search",
|
||||
is_bounce: false,
|
||||
duration: 100,
|
||||
start: ~N[2019-01-01 03:00:00],
|
||||
|
Loading…
Reference in New Issue
Block a user