Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Browser and OS version #397

Merged
merged 11 commits into from
Nov 10, 2020
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions assets/js/dashboard/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions assets/js/dashboard/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand Down
94 changes: 94 additions & 0 deletions assets/js/dashboard/stats/devices/browsers.js
Original file line number Diff line number Diff line change
@@ -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>
)
}
}

Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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)
Expand Down