mirror of
https://github.com/plausible/analytics.git
synced 2024-11-22 18:52:38 +03:00
Browser and OS version (#397)
* Collect browser and OS version * Display browser version * Show operating system versions * Device categorization * Treat headless chrome like a bot * Ignore events from automated browsers * Only take major and minor of the version * Add tests * Add CHANGELOG entry * Add changelog entry for bots * Store empty value as browser when unknown
This commit is contained in:
parent
f7885a93d7
commit
d0d7b823f8
@ -7,9 +7,11 @@ All notable changes to this project will be documented in this file.
|
||||
- Ability to add event metadata plausible/analytics#381
|
||||
- Add tracker module to automatically track outbound links plausible/analytics#389
|
||||
- Display weekday on the visitor graph plausible/analytics#175
|
||||
- Collect and display browser & OS versions plausible/analytics#397
|
||||
|
||||
### Changed
|
||||
- Use alpine as base image to decrease Docker image size plausible/analytics#353
|
||||
- Ignore automated browsers (Phantom, Selenium, Headless Chrome, etc)
|
||||
|
||||
### Fixed
|
||||
- Do not error when activating an already activated account plausible/analytics#370
|
||||
|
@ -33,9 +33,17 @@ function filterText(key, value, query) {
|
||||
if (key === "browser") {
|
||||
return <span className="inline-block max-w-sm truncate">Browser: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "browser_version") {
|
||||
const browserName = query.filters["browser"] ? query.filters["browser"] : 'Browser'
|
||||
return <span className="inline-block max-w-sm truncate">{browserName}.Version: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "os") {
|
||||
return <span className="inline-block max-w-sm truncate">Operating System: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "os_version") {
|
||||
const osName = query.filters["os"] ? query.filters["os"] : 'OS'
|
||||
return <span className="inline-block max-w-sm truncate">{osName}.Version: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "country") {
|
||||
const allCountries = Datamap.prototype.worldTopo.objects.world.geometries;
|
||||
const selectedCountry = allCountries.find((c) => c.id === value)
|
||||
|
@ -32,7 +32,9 @@ export function parseQuery(querystring, site) {
|
||||
'referrer': q.get('referrer'),
|
||||
'screen': q.get('screen'),
|
||||
'browser': q.get('browser'),
|
||||
'browser_version': q.get('browser_version'),
|
||||
'os': q.get('os'),
|
||||
'os_version': q.get('os_version'),
|
||||
'country': q.get('country'),
|
||||
'page': q.get('page')
|
||||
}
|
||||
|
94
assets/js/dashboard/stats/devices/browsers.js
Normal file
94
assets/js/dashboard/stats/devices/browsers.js
Normal file
@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import FadeIn from '../../fade-in'
|
||||
import numberFormatter from '../../number-formatter'
|
||||
import Bar from '../bar'
|
||||
import * as api from '../../api'
|
||||
|
||||
export default class Browsers extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {loading: true}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchBrowsers()
|
||||
if (this.props.timer) this.props.timer.onTick(this.fetchBrowsers.bind(this))
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.query !== prevProps.query) {
|
||||
this.setState({loading: true, browsers: null})
|
||||
this.fetchBrowsers()
|
||||
}
|
||||
}
|
||||
|
||||
fetchBrowsers() {
|
||||
if (this.props.query.filters.browser) {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/browser-versions`, this.props.query)
|
||||
.then((res) => this.setState({loading: false, browsers: res}))
|
||||
} else {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/browsers`, this.props.query)
|
||||
.then((res) => this.setState({loading: false, browsers: res}))
|
||||
}
|
||||
}
|
||||
|
||||
renderBrowser(browser) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
if (this.props.query.filters.browser) {
|
||||
query.set('browser_version', browser.name)
|
||||
} else {
|
||||
query.set('browser', browser.name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={browser.name}>
|
||||
<div className="w-full h-8" style={{maxWidth: 'calc(100% - 6rem)'}}>
|
||||
<Bar count={browser.count} all={this.state.browsers} bg="bg-green-50" />
|
||||
<span className="flex px-2" style={{marginTop: '-26px'}} >
|
||||
<Link className="block truncate hover:underline" to={{search: query.toString()}}>
|
||||
{browser.name}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium">{numberFormatter(browser.count)} <span className="inline-block text-xs w-8 text-right">({browser.percentage}%)</span></span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
label() {
|
||||
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||
}
|
||||
|
||||
|
||||
renderList() {
|
||||
const key = this.props.query.filters.browser ? this.props.query.filters.browser + ' version' : 'Browser'
|
||||
|
||||
if (this.state.browsers && this.state.browsers.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>{ key }</span>
|
||||
<span>{ this.label() }</span>
|
||||
</div>
|
||||
{ this.state.browsers && this.state.browsers.map(this.renderBrowser.bind(this)) }
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return <div className="text-center mt-44 font-medium text-gray-500">No data yet</div>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ this.state.loading && <div className="loading mt-44 mx-auto"><div></div></div> }
|
||||
<FadeIn show={!this.state.loading}>
|
||||
{ this.renderList() }
|
||||
</FadeIn>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import numberFormatter from '../number-formatter'
|
||||
import Bar from './bar'
|
||||
import MoreLink from './more-link'
|
||||
import * as api from '../api'
|
||||
import Browsers from './browsers'
|
||||
import OperatingSystems from './operating-systems'
|
||||
import FadeIn from '../../fade-in'
|
||||
import numberFormatter from '../../number-formatter'
|
||||
import Bar from '../bar'
|
||||
import MoreLink from '../more-link'
|
||||
import * as api from '../../api'
|
||||
|
||||
|
||||
function FadeIn({show, children}) {
|
||||
const className = show ? "fade-enter-active" : "fade-enter"
|
||||
|
||||
return <div className={className}>{children}</div>
|
||||
}
|
||||
|
||||
const EXPLANATION = {
|
||||
'Mobile': 'up to 576px',
|
||||
'Tablet': '576px to 992px',
|
||||
@ -114,154 +111,6 @@ class ScreenSizes extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class Browsers extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {loading: true}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchBrowsers()
|
||||
if (this.props.timer) this.props.timer.onTick(this.fetchBrowsers.bind(this))
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.query !== prevProps.query) {
|
||||
this.setState({loading: true, browsers: null})
|
||||
this.fetchBrowsers()
|
||||
}
|
||||
}
|
||||
|
||||
fetchBrowsers() {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/browsers`, this.props.query)
|
||||
.then((res) => this.setState({loading: false, browsers: res}))
|
||||
}
|
||||
|
||||
renderBrowser(browser) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('browser', browser.name)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={browser.name}>
|
||||
<div className="w-full h-8" style={{maxWidth: 'calc(100% - 6rem)'}}>
|
||||
<Bar count={browser.count} all={this.state.browsers} bg="bg-green-50" />
|
||||
<span className="flex px-2" style={{marginTop: '-26px'}} >
|
||||
<Link className="block truncate hover:underline" to={{search: query.toString()}}>
|
||||
{browser.name}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium">{numberFormatter(browser.count)} <span className="inline-block text-xs w-8 text-right">({browser.percentage}%)</span></span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
label() {
|
||||
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||
}
|
||||
|
||||
renderList() {
|
||||
if (this.state.browsers && this.state.browsers.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>Browser</span>
|
||||
<span>{ this.label() }</span>
|
||||
</div>
|
||||
{ this.state.browsers && this.state.browsers.map(this.renderBrowser.bind(this)) }
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return <div className="text-center mt-44 font-medium text-gray-500">No data yet</div>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ this.state.loading && <div className="loading mt-44 mx-auto"><div></div></div> }
|
||||
<FadeIn show={!this.state.loading}>
|
||||
{ this.renderList() }
|
||||
</FadeIn>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class OperatingSystems extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {loading: true}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchOperatingSystems()
|
||||
if (this.props.timer) this.props.timer.onTick(this.fetchOperatingSystems.bind(this))
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.query !== prevProps.query) {
|
||||
this.setState({loading: true, operatingSystems: null})
|
||||
this.fetchOperatingSystems()
|
||||
}
|
||||
}
|
||||
|
||||
fetchOperatingSystems() {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/operating-systems`, this.props.query)
|
||||
.then((res) => this.setState({loading: false, operatingSystems: res}))
|
||||
}
|
||||
|
||||
renderOperatingSystem(os) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('os', os.name)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={os.name}>
|
||||
<div className="w-full h-8" style={{maxWidth: 'calc(100% - 6rem)'}}>
|
||||
<Bar count={os.count} all={this.state.operatingSystems} bg="bg-green-50" />
|
||||
<span className="flex px-2" style={{marginTop: '-26px'}}>
|
||||
<Link className="block truncate hover:underline" to={{search: query.toString()}}>
|
||||
{os.name}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium">{numberFormatter(os.count)} <span className="inline-block text-xs w-8 text-right">({os.percentage}%)</span></span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
label() {
|
||||
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||
}
|
||||
|
||||
renderList() {
|
||||
if (this.state.operatingSystems && this.state.operatingSystems.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>Operating system</span>
|
||||
<span>{ this.label() }</span>
|
||||
</div>
|
||||
{ this.state.operatingSystems && this.state.operatingSystems.map(this.renderOperatingSystem.bind(this)) }
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return <div className="text-center mt-44 font-medium text-gray-500">No data yet</div>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ this.state.loading && <div className="loading mt-44 mx-auto"><div></div></div> }
|
||||
<FadeIn show={!this.state.loading}>
|
||||
{ this.renderList() }
|
||||
</FadeIn>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default class Devices extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
93
assets/js/dashboard/stats/devices/operating-systems.js
Normal file
93
assets/js/dashboard/stats/devices/operating-systems.js
Normal file
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import FadeIn from '../../fade-in'
|
||||
import numberFormatter from '../../number-formatter'
|
||||
import Bar from '../bar'
|
||||
import * as api from '../../api'
|
||||
|
||||
export default class OperatingSystems extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {loading: true}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchOperatingSystems()
|
||||
if (this.props.timer) this.props.timer.onTick(this.fetchOperatingSystems.bind(this))
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.query !== prevProps.query) {
|
||||
this.setState({loading: true, operatingSystems: null})
|
||||
this.fetchOperatingSystems()
|
||||
}
|
||||
}
|
||||
|
||||
fetchOperatingSystems() {
|
||||
if (this.props.query.filters.os) {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/operating-system-versions`, this.props.query)
|
||||
.then((res) => this.setState({loading: false, operatingSystems: res}))
|
||||
} else {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/operating-systems`, this.props.query)
|
||||
.then((res) => this.setState({loading: false, operatingSystems: res}))
|
||||
}
|
||||
}
|
||||
|
||||
renderOperatingSystem(os) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
if (this.props.query.filters.os) {
|
||||
query.set('os_version', os.name)
|
||||
} else {
|
||||
query.set('os', os.name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={os.name}>
|
||||
<div className="w-full h-8" style={{maxWidth: 'calc(100% - 6rem)'}}>
|
||||
<Bar count={os.count} all={this.state.operatingSystems} bg="bg-green-50" />
|
||||
<span className="flex px-2" style={{marginTop: '-26px'}}>
|
||||
<Link className="block truncate hover:underline" to={{search: query.toString()}}>
|
||||
{os.name}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium">{numberFormatter(os.count)} <span className="inline-block text-xs w-8 text-right">({os.percentage}%)</span></span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
label() {
|
||||
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const key = this.props.query.filters.os ? this.props.query.filters.os + ' version' : 'Operating system'
|
||||
|
||||
if (this.state.operatingSystems && this.state.operatingSystems.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>{ key }</span>
|
||||
<span>{ this.label() }</span>
|
||||
</div>
|
||||
{ this.state.operatingSystems && this.state.operatingSystems.map(this.renderOperatingSystem.bind(this)) }
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return <div className="text-center mt-44 font-medium text-gray-500">No data yet</div>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ this.state.loading && <div className="loading mt-44 mx-auto"><div></div></div> }
|
||||
<FadeIn show={!this.state.loading}>
|
||||
{ this.renderList() }
|
||||
</FadeIn>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,9 @@ defmodule Plausible.ClickhouseEvent do
|
||||
field :country_code, :string
|
||||
field :screen_size, :string
|
||||
field :operating_system, :string
|
||||
field :operating_system_version, :string
|
||||
field :browser, :string
|
||||
field :browser_version, :string
|
||||
|
||||
field :"meta.key", {:array, :string}
|
||||
field :"meta.value", {:array, :string}
|
||||
@ -37,7 +39,9 @@ defmodule Plausible.ClickhouseEvent do
|
||||
:pathname,
|
||||
:user_id,
|
||||
:operating_system,
|
||||
:operating_system_version,
|
||||
:browser,
|
||||
:browser_version,
|
||||
:referrer,
|
||||
:referrer_source,
|
||||
:utm_medium,
|
||||
|
@ -27,7 +27,9 @@ defmodule Plausible.ClickhouseSession do
|
||||
field :country_code, :string
|
||||
field :screen_size, :string
|
||||
field :operating_system, :string
|
||||
field :operating_system_version, :string
|
||||
field :browser, :string
|
||||
field :browser_version, :string
|
||||
field :timestamp, :naive_datetime
|
||||
end
|
||||
|
||||
@ -47,7 +49,8 @@ defmodule Plausible.ClickhouseSession do
|
||||
:length,
|
||||
:is_bounce,
|
||||
:operating_system,
|
||||
:browser,
|
||||
:operating_system_version,
|
||||
:browser_version,
|
||||
:referrer,
|
||||
:referrer_source,
|
||||
:utm_medium,
|
||||
|
@ -110,7 +110,9 @@ defmodule Plausible.Session.Store do
|
||||
country_code: event.country_code,
|
||||
screen_size: event.screen_size,
|
||||
operating_system: event.operating_system,
|
||||
operating_system_version: event.operating_system_version,
|
||||
browser: event.browser,
|
||||
browser_version: event.browser_version,
|
||||
timestamp: event.timestamp,
|
||||
start: event.timestamp
|
||||
}
|
||||
|
@ -598,6 +598,21 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
|> Enum.take(limit)
|
||||
end
|
||||
|
||||
def browser_versions(site, query, limit \\ 5) do
|
||||
ClickhouseRepo.all(
|
||||
from e in base_query_w_sessions(site, query),
|
||||
group_by: e.browser_version,
|
||||
where: e.browser_version != "",
|
||||
order_by: [desc: fragment("count")],
|
||||
select: %{
|
||||
name: e.browser_version,
|
||||
count: fragment("uniq(user_id) as count")
|
||||
}
|
||||
)
|
||||
|> add_percentages
|
||||
|> Enum.take(limit)
|
||||
end
|
||||
|
||||
def operating_systems(site, query, limit \\ 5) do
|
||||
ClickhouseRepo.all(
|
||||
from e in base_query_w_sessions(site, query),
|
||||
@ -613,6 +628,21 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
|> Enum.take(limit)
|
||||
end
|
||||
|
||||
def operating_system_versions(site, query, limit \\ 5) do
|
||||
ClickhouseRepo.all(
|
||||
from e in base_query_w_sessions(site, query),
|
||||
group_by: e.operating_system_version,
|
||||
where: e.operating_system_version != "",
|
||||
order_by: [desc: fragment("count")],
|
||||
select: %{
|
||||
name: e.operating_system_version,
|
||||
count: fragment("uniq(user_id) as count")
|
||||
}
|
||||
)
|
||||
|> add_percentages
|
||||
|> Enum.take(limit)
|
||||
end
|
||||
|
||||
def current_visitors(site, query) do
|
||||
Plausible.ClickhouseRepo.one(
|
||||
from s in base_query(site, query),
|
||||
@ -841,6 +871,14 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q =
|
||||
if query.filters["browser_version"] do
|
||||
version = query.filters["browser_version"]
|
||||
from(s in sessions_q, where: s.browser_version == ^version)
|
||||
else
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q =
|
||||
if query.filters["os"] do
|
||||
os = query.filters["os"]
|
||||
@ -849,6 +887,14 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q =
|
||||
if query.filters["os_version"] do
|
||||
version = query.filters["os_version"]
|
||||
from(s in sessions_q, where: s.operating_system_version == ^version)
|
||||
else
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q =
|
||||
if query.filters["country"] do
|
||||
country = Plausible.Stats.CountryName.to_alpha2(query.filters["country"])
|
||||
@ -898,7 +944,8 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q =
|
||||
if query.filters["source"] || query.filters['referrer'] || query.filters["utm_medium"] ||
|
||||
query.filters["utm_source"] || query.filters["utm_campaign"] || query.filters["screen"] ||
|
||||
query.filters["browser"] || query.filters["os"] || query.filters["country"] do
|
||||
query.filters["browser"] || query.filters["browser_version"] || query.filters["os"] ||
|
||||
query.filters["os_version"] || query.filters["country"] do
|
||||
from(
|
||||
e in q,
|
||||
join: sq in subquery(sessions_q),
|
||||
@ -990,6 +1037,14 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["browser_version"] do
|
||||
version = query.filters["browser_version"]
|
||||
from(s in q, where: s.browser_version == ^version)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["os"] do
|
||||
os = query.filters["os"]
|
||||
@ -998,6 +1053,14 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["os_version"] do
|
||||
version = query.filters["os_version"]
|
||||
from(s in q, where: s.operating_system_version == ^version)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["country"] do
|
||||
country = Plausible.Stats.CountryName.to_alpha2(query.filters["country"])
|
||||
@ -1072,6 +1135,14 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["browser_version"] do
|
||||
version = query.filters["browser_version"]
|
||||
from(s in q, where: s.browser_version == ^version)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["os"] do
|
||||
os = query.filters["os"]
|
||||
@ -1080,6 +1151,14 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["os_version"] do
|
||||
version = query.filters["os_version"]
|
||||
from(s in q, where: s.operating_system_version == ^version)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q =
|
||||
if query.filters["country"] do
|
||||
country = Plausible.Stats.CountryName.to_alpha2(query.filters["country"])
|
||||
|
@ -65,19 +65,15 @@ defmodule PlausibleWeb.Api.ExternalController do
|
||||
"meta" => parse_meta(params)
|
||||
}
|
||||
|
||||
uri = params["url"] && URI.parse(URI.decode(params["url"]))
|
||||
user_agent = Plug.Conn.get_req_header(conn, "user-agent") |> List.first()
|
||||
ua = user_agent && UAInspector.parse(user_agent)
|
||||
|
||||
if UAInspector.bot?(user_agent) do
|
||||
if is_bot?(ua) do
|
||||
{:ok, nil}
|
||||
else
|
||||
uri = params["url"] && URI.parse(URI.decode(params["url"]))
|
||||
query = if uri && uri.query, do: URI.decode_query(uri.query), else: %{}
|
||||
|
||||
ua =
|
||||
if user_agent do
|
||||
UAInspector.Parser.parse(user_agent)
|
||||
end
|
||||
|
||||
ref = parse_referrer(uri, params["referrer"])
|
||||
country_code = visitor_country(conn)
|
||||
salts = Plausible.Session.Salts.fetch()
|
||||
@ -96,7 +92,9 @@ defmodule PlausibleWeb.Api.ExternalController do
|
||||
utm_campaign: query["utm_campaign"] || "",
|
||||
country_code: country_code || "",
|
||||
operating_system: (ua && os_name(ua)) || "",
|
||||
operating_system_version: (ua && os_version(ua)) || "",
|
||||
browser: (ua && browser_name(ua)) || "",
|
||||
browser_version: (ua && browser_version(ua)) || "",
|
||||
screen_size: calculate_screen_size(params["screen_width"]) || "",
|
||||
"meta.key": Map.keys(params["meta"]),
|
||||
"meta.value": Map.values(params["meta"])
|
||||
@ -117,6 +115,11 @@ defmodule PlausibleWeb.Api.ExternalController do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
defp is_bot?(%UAInspector.Result.Bot{}), do: true
|
||||
defp is_bot?(%UAInspector.Result{client: %UAInspector.Result.Client{name: "Headless Chrome"}}), do: true
|
||||
defp is_bot?(_), do: false
|
||||
|
||||
defp parse_meta(params) do
|
||||
raw_meta = params["m"] || params["meta"] || params["p"] || params["props"]
|
||||
|
||||
@ -199,22 +202,48 @@ defmodule PlausibleWeb.Api.ExternalController do
|
||||
|
||||
defp browser_name(ua) do
|
||||
case ua.client do
|
||||
:unknown -> ""
|
||||
%UAInspector.Result.Client{name: "Mobile Safari"} -> "Safari"
|
||||
%UAInspector.Result.Client{name: "Chrome Mobile"} -> "Chrome"
|
||||
%UAInspector.Result.Client{name: "Chrome Mobile iOS"} -> "Chrome"
|
||||
%UAInspector.Result.Client{name: "Firefox Mobile"} -> "Firefox"
|
||||
%UAInspector.Result.Client{name: "Firefox Mobile iOS"} -> "Firefox"
|
||||
%UAInspector.Result.Client{name: "Chrome Webview"} -> "Mobile App"
|
||||
%UAInspector.Result.Client{type: "mobile app"} -> "Mobile App"
|
||||
:unknown -> nil
|
||||
client -> client.name
|
||||
end
|
||||
end
|
||||
|
||||
defp major_minor(:unknown), do: ""
|
||||
defp major_minor(version) do
|
||||
version
|
||||
|> String.split(".")
|
||||
|> Enum.take(2)
|
||||
|> Enum.join(".")
|
||||
end
|
||||
|
||||
defp browser_version(ua) do
|
||||
case ua.client do
|
||||
:unknown -> ""
|
||||
%UAInspector.Result.Client{type: "mobile app"} -> ""
|
||||
client -> major_minor(client.version)
|
||||
end
|
||||
end
|
||||
|
||||
defp os_name(ua) do
|
||||
case ua.os do
|
||||
:unknown -> nil
|
||||
:unknown -> ""
|
||||
os -> os.name
|
||||
end
|
||||
end
|
||||
|
||||
defp os_version(ua) do
|
||||
case ua.os do
|
||||
:unknown -> ""
|
||||
os -> major_minor(os.version)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_referrer_source(query, ref) do
|
||||
source = query["utm_source"] || query["source"] || query["ref"]
|
||||
source || get_source_from_referrer(ref)
|
||||
|
@ -239,6 +239,13 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
json(conn, Stats.browsers(site, query, params["limit"] || 9))
|
||||
end
|
||||
|
||||
def browser_versions(conn, params) do
|
||||
site = conn.assigns[:site]
|
||||
query = Query.from(site.timezone, params)
|
||||
|
||||
json(conn, Stats.browser_versions(site, query, params["limit"] || 9))
|
||||
end
|
||||
|
||||
def operating_systems(conn, params) do
|
||||
site = conn.assigns[:site]
|
||||
query = Query.from(site.timezone, params)
|
||||
@ -246,6 +253,13 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
json(conn, Stats.operating_systems(site, query, params["limit"] || 9))
|
||||
end
|
||||
|
||||
def operating_system_versions(conn, params) do
|
||||
site = conn.assigns[:site]
|
||||
query = Query.from(site.timezone, params)
|
||||
|
||||
json(conn, Stats.operating_system_versions(site, query, params["limit"] || 9))
|
||||
end
|
||||
|
||||
def screen_sizes(conn, params) do
|
||||
site = conn.assigns[:site]
|
||||
query = Query.from(site.timezone, params)
|
||||
|
@ -51,7 +51,9 @@ defmodule PlausibleWeb.Router do
|
||||
get "/:domain/entry-pages", StatsController, :entry_pages
|
||||
get "/:domain/countries", StatsController, :countries
|
||||
get "/:domain/browsers", StatsController, :browsers
|
||||
get "/:domain/browser-versions", StatsController, :browser_versions
|
||||
get "/:domain/operating-systems", StatsController, :operating_systems
|
||||
get "/:domain/operating-system-versions", StatsController, :operating_system_versions
|
||||
get "/:domain/screen-sizes", StatsController, :screen_sizes
|
||||
get "/:domain/conversions", StatsController, :conversions
|
||||
get "/:domain/property/:prop_name", StatsController, :prop_breakdown
|
||||
|
@ -0,0 +1,15 @@
|
||||
defmodule Plausible.ClickhouseRepo.Migrations.AddBrowserVersionAndOsVersion do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:events) do
|
||||
add :browser_version, :"LowCardinality(String)"
|
||||
add :operating_system_version, :"LowCardinality(String)"
|
||||
end
|
||||
|
||||
alter table(:sessions) do
|
||||
add :browser_version, :"LowCardinality(String)"
|
||||
add :operating_system_version, :"LowCardinality(String)"
|
||||
end
|
||||
end
|
||||
end
|
@ -1 +1 @@
|
||||
!function(i,r){"use strict";var s=i.location,o=i.document,e=o.querySelector('[src*="'+r+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function n(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(s.hostname)||"file:"===s.protocol)return console.warn("Ignoring event on localhost");var n={};n.n=e,n.u=s.href,n.d=l,n.r=o.referrer||null,n.w=i.innerWidth,t&&t.meta&&(n.m=JSON.stringify(t.meta)),t&&t.props&&(n.p=JSON.stringify(t.props));var a=new XMLHttpRequest;a.open("POST",r+"/api/event",!0),a.setRequestHeader("Content-Type","text/plain"),a.send(JSON.stringify(n)),a.onreadystatechange=function(){4==a.readyState&&t&&t.callback&&t.callback()}}function a(){n("pageview")}try{var p,c=i.history;c.pushState&&(p=c.pushState,c.pushState=function(){p.apply(this,arguments),a()},i.addEventListener("popstate",a));var u=i.plausible&&i.plausible.q||[];i.plausible=n;for(var d=0;d<u.length;d++)n.apply(this,u[d]);"prerender"===o.visibilityState?o.addEventListener("visibilitychange",function(){t||"visible"!==o.visibilityState||(t=!0,a())}):a()}catch(e){console.error(e),(new Image).src=r+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
||||
!function(i,r){"use strict";var o=i.location,s=i.document,e=s.querySelector('[src*="'+r+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function n(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(o.hostname)||"file:"===o.protocol)return console.warn("Ignoring event on localhost");var n,a;i.phantom||i._phantom||i.__nightmare||i.navigator.webdriver||((n={}).n=e,n.u=o.href,n.d=l,n.r=s.referrer||null,n.w=i.innerWidth,t&&t.meta&&(n.m=JSON.stringify(t.meta)),t&&t.props&&(n.p=JSON.stringify(t.props)),(a=new XMLHttpRequest).open("POST",r+"/api/event",!0),a.setRequestHeader("Content-Type","text/plain"),a.send(JSON.stringify(n)),a.onreadystatechange=function(){4==a.readyState&&t&&t.callback&&t.callback()})}function a(){n("pageview")}try{var p,c=i.history;c.pushState&&(p=c.pushState,c.pushState=function(){p.apply(this,arguments),a()},i.addEventListener("popstate",a));var u=i.plausible&&i.plausible.q||[];i.plausible=n;for(var h=0;h<u.length;h++)n.apply(this,u[h]);"prerender"===s.visibilityState?s.addEventListener("visibilitychange",function(){t||"visible"!==s.visibilityState||(t=!0,a())}):a()}catch(e){console.error(e),(new Image).src=r+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
@ -1 +1 @@
|
||||
!function(i,r){"use strict";var s=i.location,o=i.document,e=o.querySelector('[src*="'+r+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function n(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(s.hostname)||"file:"===s.protocol)return console.warn("Ignoring event on localhost");var n={};n.n=e,n.u=s.href,n.d=l,n.r=o.referrer||null,n.w=i.innerWidth,t&&t.meta&&(n.m=JSON.stringify(t.meta)),t&&t.props&&(n.p=JSON.stringify(t.props)),n.h=1;var a=new XMLHttpRequest;a.open("POST",r+"/api/event",!0),a.setRequestHeader("Content-Type","text/plain"),a.send(JSON.stringify(n)),a.onreadystatechange=function(){4==a.readyState&&t&&t.callback&&t.callback()}}function a(){n("pageview")}try{var c,p=i.history;p.pushState&&(c=p.pushState,p.pushState=function(){c.apply(this,arguments),a()},i.addEventListener("popstate",a)),i.addEventListener("hashchange",a);var u=i.plausible&&i.plausible.q||[];i.plausible=n;for(var h=0;h<u.length;h++)n.apply(this,u[h]);"prerender"===o.visibilityState?o.addEventListener("visibilitychange",function(){t||"visible"!==o.visibilityState||(t=!0,a())}):a()}catch(e){console.error(e),(new Image).src=r+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
||||
!function(i,r){"use strict";var s=i.location,o=i.document,e=o.querySelector('[src*="'+r+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function n(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(s.hostname)||"file:"===s.protocol)return console.warn("Ignoring event on localhost");var n,a;i.phantom||i._phantom||i.__nightmare||i.navigator.webdriver||((n={}).n=e,n.u=s.href,n.d=l,n.r=o.referrer||null,n.w=i.innerWidth,t&&t.meta&&(n.m=JSON.stringify(t.meta)),t&&t.props&&(n.p=JSON.stringify(t.props)),n.h=1,(a=new XMLHttpRequest).open("POST",r+"/api/event",!0),a.setRequestHeader("Content-Type","text/plain"),a.send(JSON.stringify(n)),a.onreadystatechange=function(){4==a.readyState&&t&&t.callback&&t.callback()})}function a(){n("pageview")}try{var p,c=i.history;c.pushState&&(p=c.pushState,c.pushState=function(){p.apply(this,arguments),a()},i.addEventListener("popstate",a)),i.addEventListener("hashchange",a);var h=i.plausible&&i.plausible.q||[];i.plausible=n;for(var u=0;u<h.length;u++)n.apply(this,h[u]);"prerender"===o.visibilityState?o.addEventListener("visibilitychange",function(){t||"visible"!==o.visibilityState||(t=!0,a())}):a()}catch(e){console.error(e),(new Image).src=r+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
@ -1 +1 @@
|
||||
!function(r,i){"use strict";var o=r.location,s=r.document,e=s.querySelector('[src*="'+i+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function a(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(o.hostname)||"file:"===o.protocol)return console.warn("Ignoring event on localhost");var a={};a.n=e,a.u=o.href,a.d=l,a.r=s.referrer||null,a.w=r.innerWidth,t&&t.meta&&(a.m=JSON.stringify(t.meta)),t&&t.props&&(a.p=JSON.stringify(t.props));var n=new XMLHttpRequest;n.open("POST",i+"/api/event",!0),n.setRequestHeader("Content-Type","text/plain"),n.send(JSON.stringify(a)),n.onreadystatechange=function(){4==n.readyState&&t&&t.callback&&t.callback()}}function n(){a("pageview")}function c(e){for(var t=e.target;t&&(void 0===t.tagName||"a"!=t.tagName.toLowerCase()||!t.href);)t=t.parentNode;t&&t.href&&plausible("Outbound Link: Click",{meta:{url:t.href}}),t.target&&!t.target.match(/^_(self|parent|top)$/i)||(setTimeout(function(){o.href=t.href},150),e.preventDefault())}try{var p,u=r.history;u.pushState&&(p=u.pushState,u.pushState=function(){p.apply(this,arguments),n()},r.addEventListener("popstate",n)),r.addEventListener("load",function(){for(var e=s.getElementsByTagName("a"),t=0;t<e.length;++t){var a=e[t];a.host!==o.host&&a.addEventListener("click",c)}});var f=r.plausible&&r.plausible.q||[];r.plausible=a;for(var d=0;d<f.length;d++)a.apply(this,f[d]);"prerender"===s.visibilityState?s.addEventListener("visibilitychange",function(){t||"visible"!==s.visibilityState||(t=!0,n())}):n()}catch(e){console.error(e),(new Image).src=i+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
||||
!function(r,i){"use strict";var o=r.location,s=r.document,e=s.querySelector('[src*="'+i+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function a(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(o.hostname)||"file:"===o.protocol)return console.warn("Ignoring event on localhost");var a,n;r.phantom||r._phantom||r.__nightmare||r.navigator.webdriver||((a={}).n=e,a.u=o.href,a.d=l,a.r=s.referrer||null,a.w=r.innerWidth,t&&t.meta&&(a.m=JSON.stringify(t.meta)),t&&t.props&&(a.p=JSON.stringify(t.props)),(n=new XMLHttpRequest).open("POST",i+"/api/event",!0),n.setRequestHeader("Content-Type","text/plain"),n.send(JSON.stringify(a)),n.onreadystatechange=function(){4==n.readyState&&t&&t.callback&&t.callback()})}function n(){a("pageview")}function p(e){for(var t=e.target;t&&(void 0===t.tagName||"a"!=t.tagName.toLowerCase()||!t.href);)t=t.parentNode;t&&t.href&&plausible("Outbound Link: Click",{meta:{url:t.href}}),t.target&&!t.target.match(/^_(self|parent|top)$/i)||(setTimeout(function(){o.href=t.href},150),e.preventDefault())}try{var c,u=r.history;u.pushState&&(c=u.pushState,u.pushState=function(){c.apply(this,arguments),n()},r.addEventListener("popstate",n)),r.addEventListener("load",function(){for(var e=s.getElementsByTagName("a"),t=0;t<e.length;++t){var a=e[t];a.host!==o.host&&a.addEventListener("click",p)}});var h=r.plausible&&r.plausible.q||[];r.plausible=a;for(var f=0;f<h.length;f++)a.apply(this,h[f]);"prerender"===s.visibilityState?s.addEventListener("visibilitychange",function(){t||"visible"!==s.visibilityState||(t=!0,n())}):n()}catch(e){console.error(e),(new Image).src=i+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
@ -1 +1 @@
|
||||
!function(i,r){"use strict";var s=i.location,o=i.document,e=o.querySelector('[src*="'+r+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function n(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(s.hostname)||"file:"===s.protocol)return console.warn("Ignoring event on localhost");var n={};n.n=e,n.u=s.href,n.d=l,n.r=o.referrer||null,n.w=i.innerWidth,t&&t.meta&&(n.m=JSON.stringify(t.meta)),t&&t.props&&(n.p=JSON.stringify(t.props));var a=new XMLHttpRequest;a.open("POST",r+"/api/event",!0),a.setRequestHeader("Content-Type","text/plain"),a.send(JSON.stringify(n)),a.onreadystatechange=function(){4==a.readyState&&t&&t.callback&&t.callback()}}function a(){n("pageview")}try{var p,c=i.history;c.pushState&&(p=c.pushState,c.pushState=function(){p.apply(this,arguments),a()},i.addEventListener("popstate",a));var u=i.plausible&&i.plausible.q||[];i.plausible=n;for(var d=0;d<u.length;d++)n.apply(this,u[d]);"prerender"===o.visibilityState?o.addEventListener("visibilitychange",function(){t||"visible"!==o.visibilityState||(t=!0,a())}):a()}catch(e){console.error(e),(new Image).src=r+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
||||
!function(i,r){"use strict";var o=i.location,s=i.document,e=s.querySelector('[src*="'+r+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function n(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(o.hostname)||"file:"===o.protocol)return console.warn("Ignoring event on localhost");var n,a;i.phantom||i._phantom||i.__nightmare||i.navigator.webdriver||((n={}).n=e,n.u=o.href,n.d=l,n.r=s.referrer||null,n.w=i.innerWidth,t&&t.meta&&(n.m=JSON.stringify(t.meta)),t&&t.props&&(n.p=JSON.stringify(t.props)),(a=new XMLHttpRequest).open("POST",r+"/api/event",!0),a.setRequestHeader("Content-Type","text/plain"),a.send(JSON.stringify(n)),a.onreadystatechange=function(){4==a.readyState&&t&&t.callback&&t.callback()})}function a(){n("pageview")}try{var p,c=i.history;c.pushState&&(p=c.pushState,c.pushState=function(){p.apply(this,arguments),a()},i.addEventListener("popstate",a));var u=i.plausible&&i.plausible.q||[];i.plausible=n;for(var h=0;h<u.length;h++)n.apply(this,u[h]);"prerender"===s.visibilityState?s.addEventListener("visibilitychange",function(){t||"visible"!==s.visibilityState||(t=!0,a())}):a()}catch(e){console.error(e),(new Image).src=r+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
@ -1 +1 @@
|
||||
!function(r,i){"use strict";var o=r.location,s=r.document,e=s.querySelector('[src*="'+i+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function a(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(o.hostname)||"file:"===o.protocol)return console.warn("Ignoring event on localhost");var a={};a.n=e,a.u=o.href,a.d=l,a.r=s.referrer||null,a.w=r.innerWidth,t&&t.meta&&(a.m=JSON.stringify(t.meta)),t&&t.props&&(a.p=JSON.stringify(t.props));var n=new XMLHttpRequest;n.open("POST",i+"/api/event",!0),n.setRequestHeader("Content-Type","text/plain"),n.send(JSON.stringify(a)),n.onreadystatechange=function(){4==n.readyState&&t&&t.callback&&t.callback()}}function n(){a("pageview")}function c(e){for(var t=e.target;t&&(void 0===t.tagName||"a"!=t.tagName.toLowerCase()||!t.href);)t=t.parentNode;t&&t.href&&plausible("Outbound Link: Click",{meta:{url:t.href}}),t.target&&!t.target.match(/^_(self|parent|top)$/i)||(setTimeout(function(){o.href=t.href},150),e.preventDefault())}try{var p,u=r.history;u.pushState&&(p=u.pushState,u.pushState=function(){p.apply(this,arguments),n()},r.addEventListener("popstate",n)),r.addEventListener("load",function(){for(var e=s.getElementsByTagName("a"),t=0;t<e.length;++t){var a=e[t];a.host!==o.host&&a.addEventListener("click",c)}});var f=r.plausible&&r.plausible.q||[];r.plausible=a;for(var d=0;d<f.length;d++)a.apply(this,f[d]);"prerender"===s.visibilityState?s.addEventListener("visibilitychange",function(){t||"visible"!==s.visibilityState||(t=!0,n())}):n()}catch(e){console.error(e),(new Image).src=i+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
||||
!function(r,i){"use strict";var o=r.location,s=r.document,e=s.querySelector('[src*="'+i+'"]'),l=e&&e.getAttribute("data-domain"),t=!1;function a(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(o.hostname)||"file:"===o.protocol)return console.warn("Ignoring event on localhost");var a,n;r.phantom||r._phantom||r.__nightmare||r.navigator.webdriver||((a={}).n=e,a.u=o.href,a.d=l,a.r=s.referrer||null,a.w=r.innerWidth,t&&t.meta&&(a.m=JSON.stringify(t.meta)),t&&t.props&&(a.p=JSON.stringify(t.props)),(n=new XMLHttpRequest).open("POST",i+"/api/event",!0),n.setRequestHeader("Content-Type","text/plain"),n.send(JSON.stringify(a)),n.onreadystatechange=function(){4==n.readyState&&t&&t.callback&&t.callback()})}function n(){a("pageview")}function p(e){for(var t=e.target;t&&(void 0===t.tagName||"a"!=t.tagName.toLowerCase()||!t.href);)t=t.parentNode;t&&t.href&&plausible("Outbound Link: Click",{meta:{url:t.href}}),t.target&&!t.target.match(/^_(self|parent|top)$/i)||(setTimeout(function(){o.href=t.href},150),e.preventDefault())}try{var c,u=r.history;u.pushState&&(c=u.pushState,u.pushState=function(){c.apply(this,arguments),n()},r.addEventListener("popstate",n)),r.addEventListener("load",function(){for(var e=s.getElementsByTagName("a"),t=0;t<e.length;++t){var a=e[t];a.host!==o.host&&a.addEventListener("click",p)}});var h=r.plausible&&r.plausible.q||[];r.plausible=a;for(var f=0;f<h.length;f++)a.apply(this,h[f]);"prerender"===s.visibilityState?s.addEventListener("visibilitychange",function(){t||"visible"!==s.visibilityState||(t=!0,n())}):n()}catch(e){console.error(e),(new Image).src=i+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
|
@ -101,6 +101,21 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
assert get_event("external-controller-test-5.com") == nil
|
||||
end
|
||||
|
||||
test "Headless Chrome is ignored", %{conn: conn} do
|
||||
params = %{
|
||||
name: "pageview",
|
||||
url: "http://www.example.com/",
|
||||
domain: "headless-chrome-test.com"
|
||||
}
|
||||
|
||||
conn
|
||||
|> put_req_header("content-type", "text/plain")
|
||||
|> put_req_header("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/85.0.4183.83 Safari/537.36")
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
assert get_event("headless-chrome-test.com") == nil
|
||||
end
|
||||
|
||||
test "parses user_agent", %{conn: conn} do
|
||||
params = %{
|
||||
name: "pageview",
|
||||
@ -118,7 +133,9 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.operating_system == "Mac"
|
||||
assert pageview.operating_system_version == "10.13"
|
||||
assert pageview.browser == "Chrome"
|
||||
assert pageview.browser_version == "70.0"
|
||||
end
|
||||
|
||||
test "parses referrer", %{conn: conn} do
|
||||
|
@ -14,4 +14,17 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/browser-versions" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "returns top browser versions by unique visitors", %{conn: conn, site: site} do
|
||||
filters = Jason.encode!(%{browser: "Chrome"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browser-versions?period=day&date=2019-01-01")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "78.0", "count" => 2, "percentage" => 100}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -14,4 +14,17 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/operating-system-versions" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "returns top OS versions by unique visitors", %{conn: conn, site: site} do
|
||||
filters = Jason.encode!(%{os: "Mac"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browser-versions?period=day&date=2019-01-01")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "10.15", "count" => 2, "percentage" => 100}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -10,7 +10,9 @@ defmodule Plausible.Test.ClickhouseSetup do
|
||||
pathname: "/",
|
||||
country_code: "EE",
|
||||
browser: "Chrome",
|
||||
browser_version: "78.0",
|
||||
operating_system: "Mac",
|
||||
operating_system_version: "10.15",
|
||||
screen_size: "Desktop",
|
||||
referrer_source: "10words",
|
||||
referrer: "10words.com/page1",
|
||||
@ -22,7 +24,9 @@ defmodule Plausible.Test.ClickhouseSetup do
|
||||
pathname: "/",
|
||||
country_code: "EE",
|
||||
browser: "Chrome",
|
||||
browser_version: "78.0",
|
||||
operating_system: "Mac",
|
||||
operating_system_version: "10.15",
|
||||
screen_size: "Desktop",
|
||||
referrer_source: "10words",
|
||||
referrer: "10words.com/page2",
|
||||
|
@ -51,9 +51,11 @@ defmodule Plausible.Factory do
|
||||
timestamp: Timex.now(),
|
||||
is_bounce: false,
|
||||
browser: "",
|
||||
browser_version: "",
|
||||
country_code: "",
|
||||
screen_size: "",
|
||||
operating_system: ""
|
||||
operating_system: "",
|
||||
operating_system_version: ""
|
||||
}
|
||||
end
|
||||
|
||||
@ -82,9 +84,11 @@ defmodule Plausible.Factory do
|
||||
utm_source: "",
|
||||
utm_campaign: "",
|
||||
browser: "",
|
||||
browser_version: "",
|
||||
country_code: "",
|
||||
screen_size: "",
|
||||
operating_system: "",
|
||||
operating_system_version: "",
|
||||
"meta.key": [],
|
||||
"meta.value": []
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
function trigger(eventName, options) {
|
||||
if (/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(location.hostname) || location.protocol === 'file:') return console.warn('Ignoring event on localhost');
|
||||
if (window.phantom || window._phantom || window.__nightmare || window.navigator.webdriver) return;
|
||||
|
||||
var payload = {}
|
||||
payload.n = eventName
|
||||
|
Loading…
Reference in New Issue
Block a user