add widget component for totals per period

This commit is contained in:
Danny van Kooten 2016-11-26 15:39:29 +01:00
parent 3c77eb14b1
commit 512a256ebd
13 changed files with 142 additions and 37 deletions

View File

@ -3,7 +3,7 @@ Ana. Open Source Web Analytics.
This is nowhere near being usable, let alone stable. Treat as a proof of concept.
![Screenshot of the Ana dashboard](https://github.com/dannyvankooten/ana/raw/master/assets/img/screenshot.png?2)
![Screenshot of the Ana dashboard](https://github.com/dannyvankooten/ana/raw/master/assets/img/screenshot.png?v=3)
## Usage

View File

@ -1,21 +1,21 @@
Ana Roadmap
===========
This is a general draft document for thoughts and todo's. This has no structure.
This is a general draft document for thoughts and todo's, without any structure to it.
### What's cooking?
- Add license file.
- Get DB creds from env.
- Compare period totals to previous period.
- Allow setting custom limit in table overviews.
- Allow sorting in table overviews.
- Choose a OS license & settle on name.
- JS client for consuming API endpoints.
- Envelope API responses.
- Error handling.
- Handle canonical URL's.
- Track canonical URL's.
- Show referrals.
- Country flags.
- Geolocate unknown IP addresses periodically.
- Mask last 2 bits of IP address.
- Geo map?
- Mask last part of IP address.
### Key metrics
@ -28,8 +28,3 @@ This is a general draft document for thoughts and todo's. This has no structure.
- Acquisition
- Referral's
- Search keywords
### Admin themes
- Pages, http://pages.revox.io/dashboard/latest/html/index.html#usa
- Metronic, http://keenthemes.com/preview/metronic/

6
ana.go
View File

@ -13,7 +13,7 @@ import (
func main() {
// test .env file
// load .env file
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
@ -28,9 +28,11 @@ func main() {
r.HandleFunc("/collect", api.CollectHandler).Methods("GET")
r.Handle("/api/session", api.Login).Methods("POST")
r.Handle("/api/session", api.Logout).Methods("DELETE")
r.Handle("/api/visits/count", api.Authorize(api.GetVisitsCountHandler)).Methods("GET")
r.Handle("/api/visits/count/day", api.Authorize(api.GetVisitsDayCountHandler)).Methods("GET")
r.Handle("/api/visits/count/realtime", api.Authorize(api.GetVisitsRealtimeCount)).Methods("GET")
r.Handle("/api/visits/count/realtime", api.Authorize(api.GetVisitsRealtimeCountHandler)).Methods("GET")
r.Handle("/api/visits", api.Authorize(api.GetVisitsHandler)).Methods("GET")
r.Handle("/api/pageviews/count", api.Authorize(api.GetPageviewsCountHandler)).Methods("GET")
r.Handle("/api/pageviews/count/day", api.Authorize(api.GetPageviewsDayCountHandler)).Methods("GET")
r.Handle("/api/pageviews", api.Authorize(api.GetPageviewsHandler)).Methods("GET")
r.Handle("/api/languages", api.Authorize(api.GetLanguagesHandler)).Methods("GET")

View File

@ -47,6 +47,15 @@ func fillDatapoints(days int, points []Datapoint) []Datapoint {
return newPoints
}
func getRequestedLimit(r *http.Request) int {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil || limit == 0 {
limit = 10
}
return limit
}
func getRequestedPeriod(r *http.Request) int {
period, err := strconv.Atoi(r.URL.Query().Get("period"))
if err != nil || period == 0 {

View File

@ -16,7 +16,6 @@ var Login = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
checkError(err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte("true"))
})
@ -29,7 +28,6 @@ var Logout = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte("true"))
})

View File

@ -43,6 +43,21 @@ var GetPageviewsHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.R
})
// URL: /api/pageviews/count
var GetPageviewsCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
period := getRequestedPeriod(r)
stmt, err := core.DB.Prepare(`SELECT COUNT(*) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? day) AND timestamp <= CURRENT_TIMESTAMP`)
checkError(err)
defer stmt.Close()
var result int
stmt.QueryRow(period).Scan(&result)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
})
// URL: /api/pageviews/count/day
var GetPageviewsDayCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
period := getRequestedPeriod(r)

View File

@ -5,7 +5,6 @@ import (
"github.com/dannyvankooten/ana/models"
"github.com/dannyvankooten/ana/core"
"encoding/json"
"strconv"
)
// URL: /api/visits
@ -26,11 +25,7 @@ var GetVisitsHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Requ
checkError(err)
defer stmt.Close()
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit == 0 {
limit = 10
}
limit := getRequestedLimit(r)
rows, err := stmt.Query(&limit)
checkError(err)
@ -50,11 +45,25 @@ var GetVisitsHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Requ
json.NewEncoder(w).Encode(results)
})
// URL: /api/visits/count/realtime
var GetVisitsRealtimeCount = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
row := core.DB.QueryRow(`SELECT COUNT(DISTINCT(ip_address)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR_MINUTE) AND timestamp <= CURRENT_TIMESTAMP`)
// URL: /api/visits/count
var GetVisitsCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
period := getRequestedPeriod(r)
stmt, err := core.DB.Prepare(`SELECT COUNT(DISTINCT(ip_address)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? day) AND timestamp <= CURRENT_TIMESTAMP`)
checkError(err)
defer stmt.Close()
var result int
row.Scan(&result)
stmt.QueryRow(period).Scan(&result)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
})
// URL: /api/visits/count/realtime
var GetVisitsRealtimeCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var result int
core.DB.QueryRow(`SELECT COUNT(DISTINCT(ip_address)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR_MINUTE) AND timestamp <= CURRENT_TIMESTAMP`).Scan(&result)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
@ -70,11 +79,7 @@ var GetVisitsDayCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *h
checkError(err)
defer stmt.Close()
period, err := strconv.Atoi(r.URL.Query().Get("period"))
if err != nil || period == 0 {
period = 1
}
period := getRequestedPeriod(r)
rows, err := stmt.Query(period)
checkError(err)
@ -89,9 +94,6 @@ var GetVisitsDayCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *h
results = fillDatapoints(period, results)
err = rows.Err();
checkError(err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(results)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -0,0 +1,46 @@
'use strict';
import { h, render, Component } from 'preact';
class CountWidget extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
this.fetchData = this.fetchData.bind(this);
this.fetchData(props.period);
}
componentWillReceiveProps(newProps) {
if(this.props.period != newProps.period) {
this.fetchData(newProps.period)
}
}
fetchData(period) {
return fetch('/api/' + this.props.endpoint + '/count?period=' + period, {
credentials: 'include'
}).then((r) => {
if( r.ok ) { return r.json(); }
throw new Error();
}).then((data) => {
this.setState({ count: data })
});
}
render() {
return (
<div class="block center-text">
<h4 class="no-margin">{this.props.title}</h4>
<div class="big">{this.state.count}</div>
<div class="muted">last {this.props.period} days</div>
</div>
)
}
}
export default CountWidget

View File

@ -12,7 +12,7 @@ class Realtime extends Component {
}
this.fetchData = this.fetchData.bind(this);
this.fetchData();
window.setInterval(this.fetchData, 6000);
window.setInterval(this.fetchData, 15000);
}
fetchData() {

View File

@ -7,6 +7,7 @@ import Graph from '../components/Graph.js';
import DatePicker from '../components/DatePicker.js';
import Table from '../components/Table.js';
import HeaderBar from '../components/HeaderBar.js';
import CountWidget from '../components/CountWidget.js';
class Dashboard extends Component {
constructor(props) {
@ -26,6 +27,14 @@ class Dashboard extends Component {
<div class="clear">
<DatePicker period={this.state.period} onChoose={(p) => { this.setState({ period: p })}} />
</div>
<div class="row">
<div class="col-2">
<CountWidget title="Visitors" endpoint="visits" period={this.state.period} />
</div>
<div class="col-2">
<CountWidget title="Pageviews" endpoint="pageviews" period={this.state.period} />
</div>
</div>
<Graph period={this.state.period} />
<div class="row">
<div class="col-4">

View File

@ -26,10 +26,22 @@
margin-bottom: 40px;
}
.no-margin {
margin: 0 !important;
}
.clear {
clear: both;
}
.big {
font-size: 32px;
}
.muted {
color: #888;
}
.center-text {
text-align: center;
}

View File

@ -15,7 +15,7 @@ body {
background: #f5f5f5;
}
h1, h2, h3 {
h1, h2, h3, h4, h5 {
color: #111;
margin: 0;
@ -84,3 +84,20 @@ a {
font-weight: bold;
font-size: 120%;
}
h1 {
font-size: 28px;
}
h2 {
font-size: 24px;
}
h3{
font-size: 20px;
}
h4 {
font-size: 16px;
}