mirror of
https://github.com/plausible/analytics.git
synced 2024-11-23 03:04:43 +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
|
- Ability to add event metadata plausible/analytics#381
|
||||||
- Add tracker module to automatically track outbound links plausible/analytics#389
|
- Add tracker module to automatically track outbound links plausible/analytics#389
|
||||||
- Display weekday on the visitor graph plausible/analytics#175
|
- Display weekday on the visitor graph plausible/analytics#175
|
||||||
|
- Collect and display browser & OS versions plausible/analytics#397
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Use alpine as base image to decrease Docker image size plausible/analytics#353
|
- Use alpine as base image to decrease Docker image size plausible/analytics#353
|
||||||
|
- Ignore automated browsers (Phantom, Selenium, Headless Chrome, etc)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Do not error when activating an already activated account plausible/analytics#370
|
- Do not error when activating an already activated account plausible/analytics#370
|
||||||
|
@ -33,9 +33,17 @@ function filterText(key, value, query) {
|
|||||||
if (key === "browser") {
|
if (key === "browser") {
|
||||||
return <span className="inline-block max-w-sm truncate">Browser: <b>{value}</b></span>
|
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") {
|
if (key === "os") {
|
||||||
return <span className="inline-block max-w-sm truncate">Operating System: <b>{value}</b></span>
|
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") {
|
if (key === "country") {
|
||||||
const allCountries = Datamap.prototype.worldTopo.objects.world.geometries;
|
const allCountries = Datamap.prototype.worldTopo.objects.world.geometries;
|
||||||
const selectedCountry = allCountries.find((c) => c.id === value)
|
const selectedCountry = allCountries.find((c) => c.id === value)
|
||||||
|
@ -32,7 +32,9 @@ export function parseQuery(querystring, site) {
|
|||||||
'referrer': q.get('referrer'),
|
'referrer': q.get('referrer'),
|
||||||
'screen': q.get('screen'),
|
'screen': q.get('screen'),
|
||||||
'browser': q.get('browser'),
|
'browser': q.get('browser'),
|
||||||
|
'browser_version': q.get('browser_version'),
|
||||||
'os': q.get('os'),
|
'os': q.get('os'),
|
||||||
|
'os_version': q.get('os_version'),
|
||||||
'country': q.get('country'),
|
'country': q.get('country'),
|
||||||
'page': q.get('page')
|
'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 React from 'react';
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
import numberFormatter from '../number-formatter'
|
import Browsers from './browsers'
|
||||||
import Bar from './bar'
|
import OperatingSystems from './operating-systems'
|
||||||
import MoreLink from './more-link'
|
import FadeIn from '../../fade-in'
|
||||||
import * as api from '../api'
|
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 = {
|
const EXPLANATION = {
|
||||||
'Mobile': 'up to 576px',
|
'Mobile': 'up to 576px',
|
||||||
'Tablet': '576px to 992px',
|
'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 {
|
export default class Devices extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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 :country_code, :string
|
||||||
field :screen_size, :string
|
field :screen_size, :string
|
||||||
field :operating_system, :string
|
field :operating_system, :string
|
||||||
|
field :operating_system_version, :string
|
||||||
field :browser, :string
|
field :browser, :string
|
||||||
|
field :browser_version, :string
|
||||||
|
|
||||||
field :"meta.key", {:array, :string}
|
field :"meta.key", {:array, :string}
|
||||||
field :"meta.value", {:array, :string}
|
field :"meta.value", {:array, :string}
|
||||||
@ -37,7 +39,9 @@ defmodule Plausible.ClickhouseEvent do
|
|||||||
:pathname,
|
:pathname,
|
||||||
:user_id,
|
:user_id,
|
||||||
:operating_system,
|
:operating_system,
|
||||||
|
:operating_system_version,
|
||||||
:browser,
|
:browser,
|
||||||
|
:browser_version,
|
||||||
:referrer,
|
:referrer,
|
||||||
:referrer_source,
|
:referrer_source,
|
||||||
:utm_medium,
|
:utm_medium,
|
||||||
|
@ -27,7 +27,9 @@ defmodule Plausible.ClickhouseSession do
|
|||||||
field :country_code, :string
|
field :country_code, :string
|
||||||
field :screen_size, :string
|
field :screen_size, :string
|
||||||
field :operating_system, :string
|
field :operating_system, :string
|
||||||
|
field :operating_system_version, :string
|
||||||
field :browser, :string
|
field :browser, :string
|
||||||
|
field :browser_version, :string
|
||||||
field :timestamp, :naive_datetime
|
field :timestamp, :naive_datetime
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -47,7 +49,8 @@ defmodule Plausible.ClickhouseSession do
|
|||||||
:length,
|
:length,
|
||||||
:is_bounce,
|
:is_bounce,
|
||||||
:operating_system,
|
:operating_system,
|
||||||
:browser,
|
:operating_system_version,
|
||||||
|
:browser_version,
|
||||||
:referrer,
|
:referrer,
|
||||||
:referrer_source,
|
:referrer_source,
|
||||||
:utm_medium,
|
:utm_medium,
|
||||||
|
@ -110,7 +110,9 @@ defmodule Plausible.Session.Store do
|
|||||||
country_code: event.country_code,
|
country_code: event.country_code,
|
||||||
screen_size: event.screen_size,
|
screen_size: event.screen_size,
|
||||||
operating_system: event.operating_system,
|
operating_system: event.operating_system,
|
||||||
|
operating_system_version: event.operating_system_version,
|
||||||
browser: event.browser,
|
browser: event.browser,
|
||||||
|
browser_version: event.browser_version,
|
||||||
timestamp: event.timestamp,
|
timestamp: event.timestamp,
|
||||||
start: event.timestamp
|
start: event.timestamp
|
||||||
}
|
}
|
||||||
|
@ -598,6 +598,21 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
|> Enum.take(limit)
|
|> Enum.take(limit)
|
||||||
end
|
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
|
def operating_systems(site, query, limit \\ 5) do
|
||||||
ClickhouseRepo.all(
|
ClickhouseRepo.all(
|
||||||
from e in base_query_w_sessions(site, query),
|
from e in base_query_w_sessions(site, query),
|
||||||
@ -613,6 +628,21 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
|> Enum.take(limit)
|
|> Enum.take(limit)
|
||||||
end
|
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
|
def current_visitors(site, query) do
|
||||||
Plausible.ClickhouseRepo.one(
|
Plausible.ClickhouseRepo.one(
|
||||||
from s in base_query(site, query),
|
from s in base_query(site, query),
|
||||||
@ -841,6 +871,14 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
sessions_q
|
sessions_q
|
||||||
end
|
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 =
|
sessions_q =
|
||||||
if query.filters["os"] do
|
if query.filters["os"] do
|
||||||
os = query.filters["os"]
|
os = query.filters["os"]
|
||||||
@ -849,6 +887,14 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
sessions_q
|
sessions_q
|
||||||
end
|
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 =
|
sessions_q =
|
||||||
if query.filters["country"] do
|
if query.filters["country"] do
|
||||||
country = Plausible.Stats.CountryName.to_alpha2(query.filters["country"])
|
country = Plausible.Stats.CountryName.to_alpha2(query.filters["country"])
|
||||||
@ -898,7 +944,8 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
q =
|
q =
|
||||||
if query.filters["source"] || query.filters['referrer'] || query.filters["utm_medium"] ||
|
if query.filters["source"] || query.filters['referrer'] || query.filters["utm_medium"] ||
|
||||||
query.filters["utm_source"] || query.filters["utm_campaign"] || query.filters["screen"] ||
|
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(
|
from(
|
||||||
e in q,
|
e in q,
|
||||||
join: sq in subquery(sessions_q),
|
join: sq in subquery(sessions_q),
|
||||||
@ -990,6 +1037,14 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
q
|
q
|
||||||
end
|
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 =
|
q =
|
||||||
if query.filters["os"] do
|
if query.filters["os"] do
|
||||||
os = query.filters["os"]
|
os = query.filters["os"]
|
||||||
@ -998,6 +1053,14 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
q
|
q
|
||||||
end
|
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 =
|
q =
|
||||||
if query.filters["country"] do
|
if query.filters["country"] do
|
||||||
country = Plausible.Stats.CountryName.to_alpha2(query.filters["country"])
|
country = Plausible.Stats.CountryName.to_alpha2(query.filters["country"])
|
||||||
@ -1072,6 +1135,14 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
q
|
q
|
||||||
end
|
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 =
|
q =
|
||||||
if query.filters["os"] do
|
if query.filters["os"] do
|
||||||
os = query.filters["os"]
|
os = query.filters["os"]
|
||||||
@ -1080,6 +1151,14 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
q
|
q
|
||||||
end
|
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 =
|
q =
|
||||||
if query.filters["country"] do
|
if query.filters["country"] do
|
||||||
country = Plausible.Stats.CountryName.to_alpha2(query.filters["country"])
|
country = Plausible.Stats.CountryName.to_alpha2(query.filters["country"])
|
||||||
|
@ -65,19 +65,15 @@ defmodule PlausibleWeb.Api.ExternalController do
|
|||||||
"meta" => parse_meta(params)
|
"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()
|
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}
|
{:ok, nil}
|
||||||
else
|
else
|
||||||
|
uri = params["url"] && URI.parse(URI.decode(params["url"]))
|
||||||
query = if uri && uri.query, do: URI.decode_query(uri.query), else: %{}
|
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"])
|
ref = parse_referrer(uri, params["referrer"])
|
||||||
country_code = visitor_country(conn)
|
country_code = visitor_country(conn)
|
||||||
salts = Plausible.Session.Salts.fetch()
|
salts = Plausible.Session.Salts.fetch()
|
||||||
@ -96,7 +92,9 @@ defmodule PlausibleWeb.Api.ExternalController do
|
|||||||
utm_campaign: query["utm_campaign"] || "",
|
utm_campaign: query["utm_campaign"] || "",
|
||||||
country_code: country_code || "",
|
country_code: country_code || "",
|
||||||
operating_system: (ua && os_name(ua)) || "",
|
operating_system: (ua && os_name(ua)) || "",
|
||||||
|
operating_system_version: (ua && os_version(ua)) || "",
|
||||||
browser: (ua && browser_name(ua)) || "",
|
browser: (ua && browser_name(ua)) || "",
|
||||||
|
browser_version: (ua && browser_version(ua)) || "",
|
||||||
screen_size: calculate_screen_size(params["screen_width"]) || "",
|
screen_size: calculate_screen_size(params["screen_width"]) || "",
|
||||||
"meta.key": Map.keys(params["meta"]),
|
"meta.key": Map.keys(params["meta"]),
|
||||||
"meta.value": Map.values(params["meta"])
|
"meta.value": Map.values(params["meta"])
|
||||||
@ -117,6 +115,11 @@ defmodule PlausibleWeb.Api.ExternalController do
|
|||||||
end
|
end
|
||||||
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
|
defp parse_meta(params) do
|
||||||
raw_meta = params["m"] || params["meta"] || params["p"] || params["props"]
|
raw_meta = params["m"] || params["meta"] || params["p"] || params["props"]
|
||||||
|
|
||||||
@ -199,22 +202,48 @@ defmodule PlausibleWeb.Api.ExternalController do
|
|||||||
|
|
||||||
defp browser_name(ua) do
|
defp browser_name(ua) do
|
||||||
case ua.client do
|
case ua.client do
|
||||||
|
:unknown -> ""
|
||||||
%UAInspector.Result.Client{name: "Mobile Safari"} -> "Safari"
|
%UAInspector.Result.Client{name: "Mobile Safari"} -> "Safari"
|
||||||
%UAInspector.Result.Client{name: "Chrome Mobile"} -> "Chrome"
|
%UAInspector.Result.Client{name: "Chrome Mobile"} -> "Chrome"
|
||||||
%UAInspector.Result.Client{name: "Chrome Mobile iOS"} -> "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"
|
%UAInspector.Result.Client{type: "mobile app"} -> "Mobile App"
|
||||||
:unknown -> nil
|
|
||||||
client -> client.name
|
client -> client.name
|
||||||
end
|
end
|
||||||
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
|
defp os_name(ua) do
|
||||||
case ua.os do
|
case ua.os do
|
||||||
:unknown -> nil
|
:unknown -> ""
|
||||||
os -> os.name
|
os -> os.name
|
||||||
end
|
end
|
||||||
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
|
defp get_referrer_source(query, ref) do
|
||||||
source = query["utm_source"] || query["source"] || query["ref"]
|
source = query["utm_source"] || query["source"] || query["ref"]
|
||||||
source || get_source_from_referrer(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))
|
json(conn, Stats.browsers(site, query, params["limit"] || 9))
|
||||||
end
|
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
|
def operating_systems(conn, params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site.timezone, params)
|
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))
|
json(conn, Stats.operating_systems(site, query, params["limit"] || 9))
|
||||||
end
|
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
|
def screen_sizes(conn, params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
query = Query.from(site.timezone, params)
|
query = Query.from(site.timezone, params)
|
||||||
|
@ -51,7 +51,9 @@ defmodule PlausibleWeb.Router do
|
|||||||
get "/:domain/entry-pages", StatsController, :entry_pages
|
get "/:domain/entry-pages", StatsController, :entry_pages
|
||||||
get "/:domain/countries", StatsController, :countries
|
get "/:domain/countries", StatsController, :countries
|
||||||
get "/:domain/browsers", StatsController, :browsers
|
get "/:domain/browsers", StatsController, :browsers
|
||||||
|
get "/:domain/browser-versions", StatsController, :browser_versions
|
||||||
get "/:domain/operating-systems", StatsController, :operating_systems
|
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/screen-sizes", StatsController, :screen_sizes
|
||||||
get "/:domain/conversions", StatsController, :conversions
|
get "/:domain/conversions", StatsController, :conversions
|
||||||
get "/:domain/property/:prop_name", StatsController, :prop_breakdown
|
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
|
assert get_event("external-controller-test-5.com") == nil
|
||||||
end
|
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
|
test "parses user_agent", %{conn: conn} do
|
||||||
params = %{
|
params = %{
|
||||||
name: "pageview",
|
name: "pageview",
|
||||||
@ -118,7 +133,9 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
|
|
||||||
assert response(conn, 202) == ""
|
assert response(conn, 202) == ""
|
||||||
assert pageview.operating_system == "Mac"
|
assert pageview.operating_system == "Mac"
|
||||||
|
assert pageview.operating_system_version == "10.13"
|
||||||
assert pageview.browser == "Chrome"
|
assert pageview.browser == "Chrome"
|
||||||
|
assert pageview.browser_version == "70.0"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "parses referrer", %{conn: conn} do
|
test "parses referrer", %{conn: conn} do
|
||||||
|
@ -14,4 +14,17 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
@ -14,4 +14,17 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
@ -10,7 +10,9 @@ defmodule Plausible.Test.ClickhouseSetup do
|
|||||||
pathname: "/",
|
pathname: "/",
|
||||||
country_code: "EE",
|
country_code: "EE",
|
||||||
browser: "Chrome",
|
browser: "Chrome",
|
||||||
|
browser_version: "78.0",
|
||||||
operating_system: "Mac",
|
operating_system: "Mac",
|
||||||
|
operating_system_version: "10.15",
|
||||||
screen_size: "Desktop",
|
screen_size: "Desktop",
|
||||||
referrer_source: "10words",
|
referrer_source: "10words",
|
||||||
referrer: "10words.com/page1",
|
referrer: "10words.com/page1",
|
||||||
@ -22,7 +24,9 @@ defmodule Plausible.Test.ClickhouseSetup do
|
|||||||
pathname: "/",
|
pathname: "/",
|
||||||
country_code: "EE",
|
country_code: "EE",
|
||||||
browser: "Chrome",
|
browser: "Chrome",
|
||||||
|
browser_version: "78.0",
|
||||||
operating_system: "Mac",
|
operating_system: "Mac",
|
||||||
|
operating_system_version: "10.15",
|
||||||
screen_size: "Desktop",
|
screen_size: "Desktop",
|
||||||
referrer_source: "10words",
|
referrer_source: "10words",
|
||||||
referrer: "10words.com/page2",
|
referrer: "10words.com/page2",
|
||||||
|
@ -51,9 +51,11 @@ defmodule Plausible.Factory do
|
|||||||
timestamp: Timex.now(),
|
timestamp: Timex.now(),
|
||||||
is_bounce: false,
|
is_bounce: false,
|
||||||
browser: "",
|
browser: "",
|
||||||
|
browser_version: "",
|
||||||
country_code: "",
|
country_code: "",
|
||||||
screen_size: "",
|
screen_size: "",
|
||||||
operating_system: ""
|
operating_system: "",
|
||||||
|
operating_system_version: ""
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -82,9 +84,11 @@ defmodule Plausible.Factory do
|
|||||||
utm_source: "",
|
utm_source: "",
|
||||||
utm_campaign: "",
|
utm_campaign: "",
|
||||||
browser: "",
|
browser: "",
|
||||||
|
browser_version: "",
|
||||||
country_code: "",
|
country_code: "",
|
||||||
screen_size: "",
|
screen_size: "",
|
||||||
operating_system: "",
|
operating_system: "",
|
||||||
|
operating_system_version: "",
|
||||||
"meta.key": [],
|
"meta.key": [],
|
||||||
"meta.value": []
|
"meta.value": []
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
function trigger(eventName, options) {
|
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 (/^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 = {}
|
var payload = {}
|
||||||
payload.n = eventName
|
payload.n = eventName
|
||||||
|
Loading…
Reference in New Issue
Block a user