mirror of
https://github.com/status-im/fathom.git
synced 2025-02-28 19:10:36 +00:00
Abstract calculating totals per category into count.Custom
This commit is contained in:
parent
00f8d2cbba
commit
4468b70601
13
ROADMAP.md
13
ROADMAP.md
@ -5,14 +5,13 @@ This is a general draft document for thoughts and todo's, without any structure
|
|||||||
|
|
||||||
### What's cooking?
|
### What's cooking?
|
||||||
|
|
||||||
- Archive `pageviews` table into daily totals for performance
|
- Never query `pageviews` table directly.
|
||||||
- Create archive on-the-fly if it does not exist yet
|
- Create archive on-the-fly if it does not exist yet?
|
||||||
- Bulk process tracking requests (Redis or in-memory?)
|
- Bulk process tracking requests (Redis or in-memory)
|
||||||
- Allow for multiple sites in same instance
|
- Allow for multiple sites in same Ana instance
|
||||||
- Update page title when it changes.
|
|
||||||
- Custom date range picker
|
- Custom date range picker
|
||||||
- Choose a better name than "Ana"
|
- Settle on a better name than "Ana"
|
||||||
- Envelope API responses
|
- Envelope API responses
|
||||||
- Visual error handling on client-side.
|
- Client-side error handling.
|
||||||
- Geolocate IP addresses periodically.
|
- Geolocate IP addresses periodically.
|
||||||
- Mask last part of IP address.
|
- Mask last part of IP address.
|
||||||
|
39
api/api.go
39
api/api.go
@ -7,12 +7,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Datapoint struct {
|
|
||||||
Count int
|
|
||||||
Label string
|
|
||||||
Percentage float64 `json:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultPeriod = 7
|
const defaultPeriod = 7
|
||||||
const defaultLimit = 10
|
const defaultLimit = 10
|
||||||
|
|
||||||
@ -23,39 +17,6 @@ func checkError(err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillDatapoints(start int64, end int64, step time.Duration, points []Datapoint) []Datapoint {
|
|
||||||
// be smart about received timestamps
|
|
||||||
if start > end {
|
|
||||||
tmp := end
|
|
||||||
end = start
|
|
||||||
start = tmp
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime := time.Unix(start, 0)
|
|
||||||
endTime := time.Unix(end, 0)
|
|
||||||
newPoints := make([]Datapoint, 0)
|
|
||||||
|
|
||||||
for startTime.Before(endTime) || startTime.Equal(endTime) {
|
|
||||||
point := Datapoint{
|
|
||||||
Count: 0,
|
|
||||||
Label: startTime.Format("2006-01-02"),
|
|
||||||
}
|
|
||||||
|
|
||||||
for j, p := range points {
|
|
||||||
if p.Label == point.Label || p.Label == startTime.Format("2006-01") {
|
|
||||||
point.Count = p.Count
|
|
||||||
points[j] = points[len(points)-1]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newPoints = append(newPoints, point)
|
|
||||||
startTime = startTime.Add(step)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newPoints
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRequestedLimit(r *http.Request) int {
|
func getRequestedLimit(r *http.Request) int {
|
||||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
if err != nil || limit == 0 {
|
if err != nil || limit == 0 {
|
||||||
|
@ -15,7 +15,7 @@ type Login struct {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var store = sessions.NewFilesystemStore( "./storage/sessions/", []byte(os.Getenv("ANA_SECRET_KEY")))
|
var store = sessions.NewFilesystemStore("./storage/sessions/", []byte(os.Getenv("ANA_SECRET_KEY")))
|
||||||
|
|
||||||
// URL: POST /api/session
|
// URL: POST /api/session
|
||||||
var LoginHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
var LoginHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -2,7 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"github.com/dannyvankooten/ana/db"
|
|
||||||
"github.com/dannyvankooten/ana/count"
|
"github.com/dannyvankooten/ana/count"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
)
|
)
|
||||||
@ -15,7 +14,7 @@ var GetBrowsersHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Re
|
|||||||
total := count.Visitors(before, after)
|
total := count.Visitors(before, after)
|
||||||
|
|
||||||
// get rows
|
// get rows
|
||||||
stmt, err := db.Conn.Prepare(`
|
results := count.Custom(`
|
||||||
SELECT
|
SELECT
|
||||||
v.browser_name,
|
v.browser_name,
|
||||||
COUNT(DISTINCT(pv.visitor_id)) AS count
|
COUNT(DISTINCT(pv.visitor_id)) AS count
|
||||||
@ -24,22 +23,7 @@ var GetBrowsersHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Re
|
|||||||
WHERE UNIX_TIMESTAMP(pv.timestamp) <= ? AND UNIX_TIMESTAMP(pv.timestamp) >= ? AND v.browser_name IS NOT NULL
|
WHERE UNIX_TIMESTAMP(pv.timestamp) <= ? AND UNIX_TIMESTAMP(pv.timestamp) >= ? AND v.browser_name IS NOT NULL
|
||||||
GROUP BY v.browser_name
|
GROUP BY v.browser_name
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
LIMIT ?`)
|
LIMIT ?`, before, after, getRequestedLimit(r), total)
|
||||||
checkError(err)
|
|
||||||
defer stmt.Close()
|
|
||||||
rows, err := stmt.Query(before, after, defaultLimit)
|
|
||||||
checkError(err)
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
results := make([]Datapoint, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var d Datapoint
|
|
||||||
err = rows.Scan(&d.Label, &d.Count);
|
|
||||||
checkError(err)
|
|
||||||
|
|
||||||
d.Percentage = float64(d.Count) / total * 100
|
|
||||||
results = append(results, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(results)
|
json.NewEncoder(w).Encode(results)
|
||||||
|
@ -2,7 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"github.com/dannyvankooten/ana/db"
|
|
||||||
"github.com/dannyvankooten/ana/count"
|
"github.com/dannyvankooten/ana/count"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
)
|
)
|
||||||
@ -15,7 +14,7 @@ var GetCountriesHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.R
|
|||||||
total := count.Visitors(before, after)
|
total := count.Visitors(before, after)
|
||||||
|
|
||||||
// get rows
|
// get rows
|
||||||
stmt, err := db.Conn.Prepare(`
|
results := count.Custom(`
|
||||||
SELECT
|
SELECT
|
||||||
v.country,
|
v.country,
|
||||||
COUNT(DISTINCT(pv.visitor_id)) AS count
|
COUNT(DISTINCT(pv.visitor_id)) AS count
|
||||||
@ -24,22 +23,7 @@ var GetCountriesHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.R
|
|||||||
WHERE UNIX_TIMESTAMP(pv.timestamp) <= ? AND UNIX_TIMESTAMP(pv.timestamp) >= ? AND v.country IS NOT NULL
|
WHERE UNIX_TIMESTAMP(pv.timestamp) <= ? AND UNIX_TIMESTAMP(pv.timestamp) >= ? AND v.country IS NOT NULL
|
||||||
GROUP BY v.country
|
GROUP BY v.country
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
LIMIT ?`)
|
LIMIT ?`, before, after, getRequestedLimit(r), total)
|
||||||
checkError(err)
|
|
||||||
defer stmt.Close()
|
|
||||||
rows, err := stmt.Query(before, after, getRequestedLimit(r))
|
|
||||||
checkError(err)
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
results := make([]Datapoint, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var d Datapoint
|
|
||||||
err = rows.Scan(&d.Label, &d.Count);
|
|
||||||
checkError(err)
|
|
||||||
|
|
||||||
d.Percentage = float64(d.Count) / total * 100
|
|
||||||
results = append(results, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(results)
|
json.NewEncoder(w).Encode(results)
|
||||||
|
@ -2,7 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"github.com/dannyvankooten/ana/db"
|
|
||||||
"github.com/dannyvankooten/ana/count"
|
"github.com/dannyvankooten/ana/count"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
)
|
)
|
||||||
@ -14,7 +13,7 @@ var GetLanguagesHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.R
|
|||||||
// get total
|
// get total
|
||||||
total := count.Visitors(before, after)
|
total := count.Visitors(before, after)
|
||||||
|
|
||||||
stmt, err := db.Conn.Prepare(`
|
results := count.Custom(`
|
||||||
SELECT
|
SELECT
|
||||||
v.browser_language,
|
v.browser_language,
|
||||||
COUNT(v.id) AS count
|
COUNT(v.id) AS count
|
||||||
@ -23,24 +22,8 @@ var GetLanguagesHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.R
|
|||||||
WHERE UNIX_TIMESTAMP(pv.timestamp) <= ? AND UNIX_TIMESTAMP(pv.timestamp) >= ?
|
WHERE UNIX_TIMESTAMP(pv.timestamp) <= ? AND UNIX_TIMESTAMP(pv.timestamp) >= ?
|
||||||
GROUP BY v.browser_language
|
GROUP BY v.browser_language
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
LIMIT ?`)
|
LIMIT ?`, before, after, getRequestedLimit(r), total,
|
||||||
checkError(err)
|
)
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
rows, err := stmt.Query(before, after, defaultLimit)
|
|
||||||
checkError(err)
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
results := make([]Datapoint, 0)
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var d Datapoint
|
|
||||||
err = rows.Scan(&d.Label, &d.Count);
|
|
||||||
checkError(err)
|
|
||||||
|
|
||||||
d.Percentage = float64(d.Count) / total * 100
|
|
||||||
results = append(results, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(results)
|
json.NewEncoder(w).Encode(results)
|
||||||
|
@ -2,12 +2,10 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
"github.com/dannyvankooten/ana/models"
|
"github.com/dannyvankooten/ana/models"
|
||||||
"github.com/dannyvankooten/ana/db"
|
"github.com/dannyvankooten/ana/db"
|
||||||
"github.com/dannyvankooten/ana/count"
|
"github.com/dannyvankooten/ana/count"
|
||||||
"encoding/json"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// URL: /api/pageviews
|
// URL: /api/pageviews
|
||||||
@ -57,40 +55,8 @@ var GetPageviewsCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *h
|
|||||||
|
|
||||||
// URL: /api/pageviews/group/day
|
// URL: /api/pageviews/group/day
|
||||||
var GetPageviewsPeriodCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
var GetPageviewsPeriodCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
|
||||||
period := vars["period"]
|
|
||||||
formats := map[string]string {
|
|
||||||
"day": "%Y-%m-%d",
|
|
||||||
"month": "%Y-%m",
|
|
||||||
}
|
|
||||||
before, after := getRequestedPeriods(r)
|
before, after := getRequestedPeriods(r)
|
||||||
stmt, err := db.Conn.Prepare(`SELECT
|
results := count.PageviewsPerDay(before, after)
|
||||||
SUM(a.count) AS count,
|
|
||||||
DATE_FORMAT(a.date, ?) AS date_group
|
|
||||||
FROM archive a
|
|
||||||
WHERE a.metric = 'pageviews' AND UNIX_TIMESTAMP(a.date) <= ? AND UNIX_TIMESTAMP(a.date) >= ?
|
|
||||||
GROUP BY date_group`)
|
|
||||||
checkError(err)
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
rows, err := stmt.Query(formats[period], before, after)
|
|
||||||
checkError(err)
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
results := make([]Datapoint, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
v := Datapoint{}
|
|
||||||
err = rows.Scan(&v.Count, &v.Label);
|
|
||||||
checkError(err)
|
|
||||||
results = append(results, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
d := time.Hour * 24;
|
|
||||||
if period == "month" {
|
|
||||||
d = d * 30
|
|
||||||
}
|
|
||||||
results = fillDatapoints(before, after, d, results)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(results)
|
json.NewEncoder(w).Encode(results)
|
||||||
})
|
})
|
||||||
|
@ -2,10 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"github.com/dannyvankooten/ana/db"
|
|
||||||
"github.com/dannyvankooten/ana/count"
|
"github.com/dannyvankooten/ana/count"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// URL: /api/referrers
|
// URL: /api/referrers
|
||||||
@ -16,7 +14,7 @@ var GetReferrersHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.R
|
|||||||
total := count.Visitors(before, after)
|
total := count.Visitors(before, after)
|
||||||
|
|
||||||
// get rows
|
// get rows
|
||||||
stmt, err := db.Conn.Prepare(`
|
results := count.Custom(`
|
||||||
SELECT
|
SELECT
|
||||||
pv.referrer_url,
|
pv.referrer_url,
|
||||||
COUNT(DISTINCT(pv.visitor_id)) AS count
|
COUNT(DISTINCT(pv.visitor_id)) AS count
|
||||||
@ -26,25 +24,7 @@ var GetReferrersHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.R
|
|||||||
AND pv.referrer_url != ""
|
AND pv.referrer_url != ""
|
||||||
GROUP BY pv.referrer_url
|
GROUP BY pv.referrer_url
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
LIMIT ?`)
|
LIMIT ?`, before, after, getRequestedLimit(r), total)
|
||||||
checkError(err)
|
|
||||||
defer stmt.Close()
|
|
||||||
rows, err := stmt.Query(before, after, getRequestedLimit(r))
|
|
||||||
checkError(err)
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
results := make([]Datapoint, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var d Datapoint
|
|
||||||
err = rows.Scan(&d.Label, &d.Count);
|
|
||||||
d.Label = strings.Replace(d.Label, "http://", "", 1)
|
|
||||||
d.Label = strings.Replace(d.Label, "https://", "", 1)
|
|
||||||
d.Label = strings.TrimRight(d.Label, "/")
|
|
||||||
checkError(err)
|
|
||||||
|
|
||||||
d.Percentage = float64(d.Count) / total * 100
|
|
||||||
results = append(results, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(results)
|
json.NewEncoder(w).Encode(results)
|
||||||
|
@ -2,7 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"github.com/dannyvankooten/ana/db"
|
|
||||||
"github.com/dannyvankooten/ana/count"
|
"github.com/dannyvankooten/ana/count"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
)
|
)
|
||||||
@ -15,7 +14,7 @@ var GetScreenResolutionsHandler = http.HandlerFunc(func(w http.ResponseWriter, r
|
|||||||
total := count.Visitors(before, after)
|
total := count.Visitors(before, after)
|
||||||
|
|
||||||
// get rows
|
// get rows
|
||||||
stmt, err := db.Conn.Prepare(`
|
results := count.Custom(`
|
||||||
SELECT
|
SELECT
|
||||||
v.screen_resolution,
|
v.screen_resolution,
|
||||||
COUNT(DISTINCT(pv.visitor_id)) AS count
|
COUNT(DISTINCT(pv.visitor_id)) AS count
|
||||||
@ -24,22 +23,7 @@ var GetScreenResolutionsHandler = http.HandlerFunc(func(w http.ResponseWriter, r
|
|||||||
WHERE UNIX_TIMESTAMP(pv.timestamp) <= ? AND UNIX_TIMESTAMP(pv.timestamp) >= ?
|
WHERE UNIX_TIMESTAMP(pv.timestamp) <= ? AND UNIX_TIMESTAMP(pv.timestamp) >= ?
|
||||||
GROUP BY v.screen_resolution
|
GROUP BY v.screen_resolution
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
LIMIT ?`)
|
LIMIT ?`, before, after, getRequestedLimit(r), total)
|
||||||
checkError(err)
|
|
||||||
defer stmt.Close()
|
|
||||||
rows, err := stmt.Query(before, after, getRequestedLimit(r))
|
|
||||||
checkError(err)
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
results := make([]Datapoint, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var d Datapoint
|
|
||||||
err = rows.Scan(&d.Label, &d.Count);
|
|
||||||
checkError(err)
|
|
||||||
|
|
||||||
d.Percentage = float64(d.Count) / total * 100
|
|
||||||
results = append(results, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(results)
|
json.NewEncoder(w).Encode(results)
|
||||||
|
@ -5,8 +5,6 @@ import (
|
|||||||
"github.com/dannyvankooten/ana/db"
|
"github.com/dannyvankooten/ana/db"
|
||||||
"github.com/dannyvankooten/ana/count"
|
"github.com/dannyvankooten/ana/count"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// URL: /api/visitors/count
|
// URL: /api/visitors/count
|
||||||
@ -30,40 +28,8 @@ var GetVisitorsRealtimeCountHandler = http.HandlerFunc(func(w http.ResponseWrite
|
|||||||
|
|
||||||
// URL: /api/visitors/count/group/:period
|
// URL: /api/visitors/count/group/:period
|
||||||
var GetVisitorsPeriodCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
var GetVisitorsPeriodCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
|
||||||
period := vars["period"]
|
|
||||||
formats := map[string]string {
|
|
||||||
"day": "%Y-%m-%d",
|
|
||||||
"month": "%Y-%m",
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt, err := db.Conn.Prepare(`SELECT
|
|
||||||
SUM(a.count) AS count,
|
|
||||||
DATE_FORMAT(a.date, ?) AS date_group
|
|
||||||
FROM archive a
|
|
||||||
WHERE a.metric = 'visitors' AND UNIX_TIMESTAMP(a.date) <= ? AND UNIX_TIMESTAMP(a.date) >= ?
|
|
||||||
GROUP BY date_group`)
|
|
||||||
checkError(err)
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
before, after := getRequestedPeriods(r)
|
before, after := getRequestedPeriods(r)
|
||||||
rows, err := stmt.Query(formats[period], before, after)
|
results := count.VisitorsPerDay(before, after)
|
||||||
checkError(err)
|
|
||||||
|
|
||||||
results := make([]Datapoint, 0)
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
v := Datapoint{}
|
|
||||||
err = rows.Scan(&v.Count, &v.Label);
|
|
||||||
checkError(err)
|
|
||||||
results = append(results, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
d := time.Hour * 24;
|
|
||||||
if period == "month" {
|
|
||||||
d = d * 30
|
|
||||||
}
|
|
||||||
results = fillDatapoints(before, after, d, results)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(results)
|
json.NewEncoder(w).Encode(results)
|
||||||
})
|
})
|
||||||
|
@ -22,12 +22,12 @@ function Chart(element, showPrimary, showSecondary) {
|
|||||||
|
|
||||||
var pageviewTip = d3.tip()
|
var pageviewTip = d3.tip()
|
||||||
.attr('class', 'd3-tip')
|
.attr('class', 'd3-tip')
|
||||||
.html((d) => '<span>' + numbers.formatWithComma(d.Count) + '</span>' + ' pageviews')
|
.html((d) => '<span>' + numbers.formatWithComma(d.Value) + '</span>' + ' pageviews')
|
||||||
.offset([-12, 0]);
|
.offset([-12, 0]);
|
||||||
|
|
||||||
var visitorTip = d3.tip()
|
var visitorTip = d3.tip()
|
||||||
.attr('class', 'd3-tip')
|
.attr('class', 'd3-tip')
|
||||||
.html((d) => '<span>' + numbers.formatWithComma(d.Count) + '</span>' + ' visitors' )
|
.html((d) => '<span>' + numbers.formatWithComma(d.Value) + '</span>' + ' visitors' )
|
||||||
.offset([-12, 0]);
|
.offset([-12, 0]);
|
||||||
|
|
||||||
var graph = d3.select('#graph');
|
var graph = d3.select('#graph');
|
||||||
@ -52,7 +52,7 @@ function Chart(element, showPrimary, showSecondary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
var max = d3.max(showPrimary ? primaryData : secondaryData, (d) => d.Count);
|
var max = d3.max(showPrimary ? primaryData : secondaryData, (d) => d.Value);
|
||||||
var ticks = primaryData.length;
|
var ticks = primaryData.length;
|
||||||
var xTick = Math.round(ticks / 7);
|
var xTick = Math.round(ticks / 7);
|
||||||
|
|
||||||
@ -86,8 +86,8 @@ function Chart(element, showPrimary, showSecondary) {
|
|||||||
|
|
||||||
bars.append('rect')
|
bars.append('rect')
|
||||||
.attr('width', x.bandwidth())
|
.attr('width', x.bandwidth())
|
||||||
.attr('height', (d) => (h - y(d.Count)) )
|
.attr('height', (d) => (h - y(d.Value)) )
|
||||||
.attr('y', (d) => y(d.Count))
|
.attr('y', (d) => y(d.Value))
|
||||||
.on('mouseover', pageviewTip.show)
|
.on('mouseover', pageviewTip.show)
|
||||||
.on('mouseout', pageviewTip.hide);
|
.on('mouseout', pageviewTip.hide);
|
||||||
}
|
}
|
||||||
@ -102,8 +102,8 @@ function Chart(element, showPrimary, showSecondary) {
|
|||||||
|
|
||||||
visitorBars.append('rect')
|
visitorBars.append('rect')
|
||||||
.attr('width', x.bandwidth() * 0.66 )
|
.attr('width', x.bandwidth() * 0.66 )
|
||||||
.attr('height', (d) => (h - y(d.Count)) )
|
.attr('height', (d) => (h - y(d.Value)) )
|
||||||
.attr('y', (d) => y(d.Count))
|
.attr('y', (d) => y(d.Value))
|
||||||
.on('mouseover', visitorTip.show)
|
.on('mouseover', visitorTip.show)
|
||||||
.on('mouseout', visitorTip.hide);
|
.on('mouseout', visitorTip.hide);
|
||||||
}
|
}
|
||||||
|
@ -52,8 +52,9 @@ class Table extends Component {
|
|||||||
const after = before - ( period * dayInSeconds );
|
const after = before - ( period * dayInSeconds );
|
||||||
|
|
||||||
Client.request(`${this.props.endpoint}?before=${before}&after=${after}&limit=${limit}`)
|
Client.request(`${this.props.endpoint}?before=${before}&after=${after}&limit=${limit}`)
|
||||||
.then((d) => { this.setState({ loading: false, records: d })})
|
.then((d) => {
|
||||||
.catch((e) => { console.log(e) })
|
this.setState({ loading: false, records: d }
|
||||||
|
)}).catch((e) => { console.log(e) })
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -61,8 +62,8 @@ class Table extends Component {
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="muted">{i+1}</td>
|
<td class="muted">{i+1}</td>
|
||||||
{this.labelCell(p)}
|
{this.labelCell(p)}
|
||||||
<td>{numbers.formatWithComma(p.Count)}</td>
|
<td>{numbers.formatWithComma(p.Value)}</td>
|
||||||
<td>{Math.round(p.Percentage)}%</td>
|
<td>{Math.round(p.PercentageValue)}%</td>
|
||||||
</tr>
|
</tr>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class Dashboard extends Component {
|
|||||||
<Table period={this.state.period} endpoint="screen-resolutions" title="Screen Resolutions" headers={["#", "Resolution", "Count", "%"]} />
|
<Table period={this.state.period} endpoint="screen-resolutions" title="Screen Resolutions" headers={["#", "Resolution", "Count", "%"]} />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<Table period={this.state.period} endpoint="referrers" title="Referrers" headers={["#", "URL", "Count", "%"]} labelCell={(p) => ( <td><a href={"//" + p.Label}>{p.Label.substring(0, 15)}</a></td>)} />
|
<Table period={this.state.period} endpoint="referrers" title="Referrers" headers={["#", "URL", "Count", "%"]} labelCell={(p) => ( <td><a href={"//" + p.Label}>{p.Label.substring(0, 15).replace('https://', '').replace('http://', '')}</a></td>)} />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<Table period={this.state.period} endpoint="browsers" title="Browsers" headers={["#", "Browser", "Count", "%"]} onAuthError={this.props.onLogout} />
|
<Table period={this.state.period} endpoint="browsers" title="Browsers" headers={["#", "Browser", "Count", "%"]} onAuthError={this.props.onLogout} />
|
||||||
|
@ -4,6 +4,7 @@ import(
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
"github.com/dannyvankooten/ana/db"
|
"github.com/dannyvankooten/ana/db"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Archive struct {
|
type Archive struct {
|
||||||
@ -14,6 +15,12 @@ type Archive struct {
|
|||||||
Date string
|
Date string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Point struct {
|
||||||
|
Label string
|
||||||
|
Value int
|
||||||
|
PercentageValue float64
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Archive) Save(Conn *sql.DB) error {
|
func (a *Archive) Save(Conn *sql.DB) error {
|
||||||
stmt, err := db.Conn.Prepare(`INSERT INTO archive(
|
stmt, err := db.Conn.Prepare(`INSERT INTO archive(
|
||||||
metric,
|
metric,
|
||||||
@ -113,3 +120,65 @@ func checkError(err error) {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Custom(sql string, before int64, after int64, limit int, total float64) []Point {
|
||||||
|
stmt, err := db.Conn.Prepare(sql)
|
||||||
|
checkError(err)
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.Query(before, after, limit)
|
||||||
|
checkError(err)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
results := newPointSlice(rows, total)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPointSlice(rows *sql.Rows, total float64) []Point {
|
||||||
|
results := make([]Point, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var d Point
|
||||||
|
err := rows.Scan(&d.Label, &d.Value);
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
d.PercentageValue = float64(d.Value) / total * 100
|
||||||
|
results = append(results, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func fill(start int64, end int64, points []Point) []Point {
|
||||||
|
// be smart about received timestamps
|
||||||
|
if start > end {
|
||||||
|
tmp := end
|
||||||
|
end = start
|
||||||
|
start = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Unix(start, 0)
|
||||||
|
endTime := time.Unix(end, 0)
|
||||||
|
newPoints := make([]Point, 0)
|
||||||
|
step := time.Hour * 24
|
||||||
|
|
||||||
|
for startTime.Before(endTime) || startTime.Equal(endTime) {
|
||||||
|
point := Point{
|
||||||
|
Value: 0,
|
||||||
|
Label: startTime.Format("2006-01-02"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, p := range points {
|
||||||
|
if p.Label == point.Label || p.Label == startTime.Format("2006-01") {
|
||||||
|
point.Value = p.Value
|
||||||
|
points[j] = points[len(points)-1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newPoints = append(newPoints, point)
|
||||||
|
startTime = startTime.Add(step)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPoints
|
||||||
|
}
|
@ -17,3 +17,30 @@ func Pageviews(before int64, after int64) float64 {
|
|||||||
stmt.QueryRow(before, after).Scan(&total)
|
stmt.QueryRow(before, after).Scan(&total)
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PageviewsPerDay(before int64, after int64) []Point {
|
||||||
|
stmt, err := db.Conn.Prepare(`SELECT
|
||||||
|
SUM(a.count) AS count,
|
||||||
|
DATE_FORMAT(a.date, '%Y-%m-%d') AS date_group
|
||||||
|
FROM archive a
|
||||||
|
WHERE a.metric = 'pageviews' AND UNIX_TIMESTAMP(a.date) <= ? AND UNIX_TIMESTAMP(a.date) >= ?
|
||||||
|
GROUP BY date_group`)
|
||||||
|
checkError(err)
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.Query(before, after)
|
||||||
|
checkError(err)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
results := make([]Point, 0)
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
p := Point{}
|
||||||
|
err = rows.Scan(&p.Value, &p.Label);
|
||||||
|
checkError(err)
|
||||||
|
results = append(results, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
results = fill(after, before, results)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
@ -17,3 +17,30 @@ func Visitors(before int64, after int64) float64 {
|
|||||||
stmt.QueryRow(before, after).Scan(&total)
|
stmt.QueryRow(before, after).Scan(&total)
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func VisitorsPerDay(before int64, after int64) ([]Point) {
|
||||||
|
stmt, err := db.Conn.Prepare(`SELECT
|
||||||
|
SUM(a.count) AS count,
|
||||||
|
DATE_FORMAT(a.date, '%Y-%m-%d') AS date_group
|
||||||
|
FROM archive a
|
||||||
|
WHERE a.metric = 'visitors' AND UNIX_TIMESTAMP(a.date) <= ? AND UNIX_TIMESTAMP(a.date) >= ?
|
||||||
|
GROUP BY date_group`)
|
||||||
|
checkError(err)
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.Query(before, after)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
results := make([]Point, 0)
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
p := Point{}
|
||||||
|
err = rows.Scan(&p.Value, &p.Label);
|
||||||
|
checkError(err)
|
||||||
|
results = append(results, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
results = fill(after, before, results)
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user