Skip to content

Commit

Permalink
Realtime dashboard improvements (#2445)
Browse files Browse the repository at this point in the history
* add a new realtime-update-timer module

* hook to the new 'tick' event in ListReport for auto-updates

This commit fixes the bug where all reports using the `ListReport` component did not
auto-update in realtime mode. Those reports are:

- Pages (Top / Entry / Exit)
- Locations (Countries / Regions / Cities)
- Devices (Screen Sizes / Browsers + versions / OS-s + versions)

* fetch data for ListReports only when scrolled into view

* refactor fetching data in ListReport

* refer to one source of truth for utm tags

* make the 'All' tab in Sources auto-update

* make all UTM tabs in Sources auto-update

* fetch UTM data only when scrolled into view

* auto-update Referrers with the new timer

* auto-update google search terms

* auto-update Conversions

* make countries map auto-update

* auto-update visitor-graph and top stats with new timer

* use new tick event for current visitors (in Historical)

* remove the old timer class

* update changelog

* Visual improvements to automatic realtime updates (#2532)

* minor consistency fix for text color in dark mode

* use FlipMove in goal conversions report

* use FlipMove in ListReports

* set main graph and top stats loading state correctly

* refactor isIntervalValid function

* enforce intervals are valid when set and stored

* remove duplicate data fetching on interval change

Fetching new data is handled by the `fetchGraphData` callback in `updateInterval`

* refactor updateMetric function

* make it clearer why 'metric' can be a faulty value

* extract 'query' and 'site' variables from 'this.props'

* reset interval state only when period is changed

The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces
all those function calls with 'fetchGraphData' which better describes the actual behavior.
We should only worry about rolling back the interval if 'query.period' has changed.

This commit also stops the graph from flickering when it is updated in realtime.

* update names of two variables

* remove unnecessary negation

* make collapsed graph state more explicit

* consider stored invalid intervals when graph mounts

* fix not showing loading spinner regression

* remove interval state from VisitorGraph (#2540)

* Realtime prop breakdown (#2535)

* disable load more in realtime mode

* extract doFetch function

* separate fetchPropBreakdown and fetchNextPage functions

* subscribe for auto-updates in realtime

* improve readability with function name changes
  • Loading branch information
RobertJoonas authored Jan 2, 2023
1 parent 22e2ae1 commit 47e2112
Show file tree
Hide file tree
Showing 19 changed files with 212 additions and 151 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file.

## Unreleased

### Fixed
- Automatically update all visible dashboard reports in the realtime view

### Changed
- Reject events with long URIs and data URIs plausible/analytics#2536
- Always show direct traffic in sources reports plausible/analytics#2531
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/historical.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function Historical(props) {
<div className="items-center w-full flex">
<div className="flex items-center w-full">
<SiteSwitcher site={props.site} loggedIn={props.loggedIn} currentUserRole={props.currentUserRole} />
<CurrentVisitors timer={props.timer} site={props.site} query={props.query} />
<CurrentVisitors site={props.site} query={props.query} />
<Filters className="flex" site={props.site} query={props.query} history={props.history} />
</div>
<Datepicker site={props.site} query={props.query} />
Expand Down
24 changes: 2 additions & 22 deletions assets/js/dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,11 @@ import {parseQuery} from './query'
import * as api from './api'
import { withComparisonProvider } from './comparison-provider-hoc';

const THIRTY_SECONDS = 30000

class Timer {
constructor() {
this.listeners = []
this.intervalId = setInterval(this.dispatchTick.bind(this), THIRTY_SECONDS)
}

onTick(listener) {
this.listeners.push(listener)
}

dispatchTick() {
for (const listener of this.listeners) {
listener()
}
}
}

class Dashboard extends React.Component {
constructor(props) {
super(props)
this.state = {
query: parseQuery(props.location.search, this.props.site),
timer: new Timer()
}
}

Expand All @@ -44,9 +24,9 @@ class Dashboard extends React.Component {

render() {
if (this.state.query.period === 'realtime') {
return <Realtime timer={this.state.timer} site={this.props.site} loggedIn={this.props.loggedIn} currentUserRole={this.props.currentUserRole} query={this.state.query} />
return <Realtime site={this.props.site} loggedIn={this.props.loggedIn} currentUserRole={this.props.currentUserRole} query={this.state.query} />
} else {
return <Historical timer={this.state.timer} site={this.props.site} loggedIn={this.props.loggedIn} currentUserRole={this.props.currentUserRole} query={this.state.query} />
return <Historical site={this.props.site} loggedIn={this.props.loggedIn} currentUserRole={this.props.currentUserRole} query={this.state.query} />
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions assets/js/dashboard/mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import 'url-search-params-polyfill';
import Router from './router'
import ErrorBoundary from './error-boundary'
import * as api from './api'
import * as timer from './util/realtime-update-timer'

timer.start()

const container = document.getElementById('stats-react-container')

Expand Down
10 changes: 5 additions & 5 deletions assets/js/dashboard/realtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ class Realtime extends React.Component {
<Datepicker site={this.props.site} query={this.props.query} />
</div>
</div>
<VisitorGraph site={this.props.site} query={this.props.query} timer={this.props.timer} />
<VisitorGraph site={this.props.site} query={this.props.query} />
<div className="items-start justify-between block w-full md:flex">
<Sources site={this.props.site} query={this.props.query} timer={this.props.timer} />
<Pages site={this.props.site} query={this.props.query} timer={this.props.timer} />
<Sources site={this.props.site} query={this.props.query} />
<Pages site={this.props.site} query={this.props.query} />
</div>
<div className="items-start justify-between block w-full md:flex">
<Locations site={this.props.site} query={this.props.query} timer={this.props.timer} />
<Devices site={this.props.site} query={this.props.query} timer={this.props.timer} />
<Locations site={this.props.site} query={this.props.query} />
<Devices site={this.props.site} query={this.props.query} />
</div>

{ this.renderConversions() }
Expand Down
13 changes: 10 additions & 3 deletions assets/js/dashboard/stats/conversions/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom'
import FlipMove from 'react-flip-move'


import Bar from '../bar'
import PropBreakdown from './prop-breakdown'
Expand All @@ -20,7 +22,7 @@ export default class Conversions extends React.Component {
viewport: DEFAULT_WIDTH,
}
this.onVisible = this.onVisible.bind(this)

this.fetchConversions = this.fetchConversions.bind(this)
this.handleResize = this.handleResize.bind(this);
}

Expand All @@ -31,6 +33,7 @@ export default class Conversions extends React.Component {

componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false);
document.removeEventListener('tick', this.fetchConversions)
}

handleResize() {
Expand All @@ -39,6 +42,9 @@ export default class Conversions extends React.Component {

onVisible() {
this.fetchConversions()
if (this.props.query.period === 'realtime') {
document.addEventListener('tick', this.fetchConversions)
}
}

componentDidUpdate(prevProps) {
Expand Down Expand Up @@ -102,8 +108,9 @@ export default class Conversions extends React.Component {
<span className="inline-block w-20">CR</span>
</div>
</div>

{ this.state.goals.map(this.renderGoal.bind(this)) }
<FlipMove>
{ this.state.goals.map(this.renderGoal.bind(this)) }
</FlipMove>
</React.Fragment>
)
}
Expand Down
47 changes: 34 additions & 13 deletions assets/js/dashboard/stats/conversions/prop-breakdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as api from '../../api'

const MOBILE_UPPER_WIDTH = 767
const DEFAULT_WIDTH = 1080
const BREAKDOWN_LIMIT = 100

// https://stackoverflow.com/a/43467144
function isValidHttpUrl(string) {
Expand Down Expand Up @@ -45,17 +46,25 @@ export default class PropertyBreakdown extends React.Component {
}

this.handleResize = this.handleResize.bind(this);
this.fetch = this.fetch.bind(this)
this.fetchAndReplace = this.fetchAndReplace.bind(this)
this.fetchAndConcat = this.fetchAndConcat.bind(this)
}

componentDidMount() {
window.addEventListener('resize', this.handleResize, false);

this.handleResize();
this.fetchPropBreakdown()
this.fetchAndReplace()

if (this.props.query.period === 'realtime') {
document.addEventListener('tick', this.fetchAndReplace)
}
}

componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false);
document.removeEventListener('tick', this.fetchAndReplace)
}

handleResize() {
Expand All @@ -67,19 +76,31 @@ export default class PropertyBreakdown extends React.Component {
return viewport > MOBILE_UPPER_WIDTH ? "16rem" : "10rem";
}

fetchPropBreakdown() {
if (this.props.query.filters['goal']) {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/property/${encodeURIComponent(this.state.propKey)}`, this.props.query, {limit: 100, page: this.state.page})
.then((res) => this.setState((state) => ({
loading: false,
breakdown: state.breakdown.concat(res),
moreResultsAvailable: res.length === 100
})))
}
fetch({concat}) {
if (!this.props.query.filters['goal']) return

api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/property/${encodeURIComponent(this.state.propKey)}`, this.props.query, {limit: BREAKDOWN_LIMIT, page: this.state.page})
.then((res) => {
let breakdown = concat ? this.state.breakdown.concat(res) : res

this.setState(() => ({
loading: false,
breakdown: breakdown,
moreResultsAvailable: res.length >= BREAKDOWN_LIMIT
}))
})
}

fetchAndReplace() {
this.fetch({concat: false})
}

fetchAndConcat() {
this.fetch({concat: true})
}

loadMore() {
this.setState({loading: true, page: this.state.page + 1}, this.fetchPropBreakdown.bind(this))
this.setState({loading: true, page: this.state.page + 1}, this.fetchAndConcat.bind(this))
}

renderUrl(value) {
Expand Down Expand Up @@ -143,13 +164,13 @@ export default class PropertyBreakdown extends React.Component {

changePropKey(newKey) {
storage.setItem(this.storageKey, newKey)
this.setState({propKey: newKey, loading: true, breakdown: [], page: 1, moreResultsAvailable: false}, this.fetchPropBreakdown)
this.setState({propKey: newKey, loading: true, breakdown: [], page: 1, moreResultsAvailable: false}, this.fetchAndReplace)
}

renderLoading() {
if (this.state.loading) {
return <div className="px-4 py-2"><div className="loading sm mx-auto"><div></div></div></div>
} else if (this.state.moreResultsAvailable) {
} else if (this.state.moreResultsAvailable && this.props.query.period !== 'realtime') {
return (
<div className="w-full text-center my-4">
<button onClick={this.loadMore.bind(this)} type="button" className="button">
Expand Down
7 changes: 6 additions & 1 deletion assets/js/dashboard/stats/current-visitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ export default class CurrentVisitors extends React.Component {
constructor(props) {
super(props)
this.state = {currentVisitors: null}
this.updateCount = this.updateCount.bind(this)
}

componentDidMount() {
this.updateCount()
this.props.timer.onTick(this.updateCount.bind(this))
document.addEventListener('tick', this.updateCount)
}

componentWillUnmount() {
document.removeEventListener('tick', this.updateCount)
}

updateCount() {
Expand Down
10 changes: 5 additions & 5 deletions assets/js/dashboard/stats/devices/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,18 @@ export default class Devices extends React.Component {
switch (this.state.mode) {
case 'browser':
if (this.props.query.filters.browser) {
return <BrowserVersions site={this.props.site} query={this.props.query} timer={this.props.timer} />
return <BrowserVersions site={this.props.site} query={this.props.query} />
}
return <Browsers site={this.props.site} query={this.props.query} timer={this.props.timer} />
return <Browsers site={this.props.site} query={this.props.query} />
case 'os':
if (this.props.query.filters.os) {
return <OperatingSystemVersions site={this.props.site} query={this.props.query} timer={this.props.timer} />
return <OperatingSystemVersions site={this.props.site} query={this.props.query} />
}
return <OperatingSystems site={this.props.site} query={this.props.query} timer={this.props.timer} />
return <OperatingSystems site={this.props.site} query={this.props.query} />
case 'size':
default:
return (
<ScreenSizes site={this.props.site} query={this.props.query} timer={this.props.timer} />
<ScreenSizes site={this.props.site} query={this.props.query} />
)
}
}
Expand Down
2 changes: 0 additions & 2 deletions assets/js/dashboard/stats/graph/graph-util.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import numberFormatter, {durationFormatter} from '../../util/number-formatter'
import dateFormatter from './date-formatter.js'

export const INTERVALS = ["month", "week", "date", "hour", "minute"]

export const METRIC_MAPPING = {
'Unique visitors (last 30 min)': 'visitors',
'Pageviews (last 30 min)': 'pageviews',
Expand Down
Loading

0 comments on commit 47e2112

Please sign in to comment.