aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelix Hanley <felix@userspace.com.au>2020-02-21 08:27:42 +0000
committerFelix Hanley <felix@userspace.com.au>2020-02-21 08:27:42 +0000
commitb25a204138d82d0b8c29eaf81003cc6b5553d58f (patch)
tree4b4bebcf5f8ce807a463ab97cb7768da07b5aee5
parentd68c6aad01c2f6e7f30389536152e1f96682b5cd (diff)
downloadsws-b25a204138d82d0b8c29eaf81003cc6b5553d58f.tar.gz
sws-b25a204138d82d0b8c29eaf81003cc6b5553d58f.tar.bz2
Add HitSet and associated interfaces for charts and reuse
-rw-r--r--charts.go8
-rw-r--r--cmd/server/charts.go4
-rw-r--r--cmd/server/sites.go24
-rw-r--r--counter/sws.js28
-rw-r--r--counter/test.html19
-rw-r--r--hit.go15
-rw-r--r--hit_set.go194
-rw-r--r--page.go44
-rw-r--r--page_set.go59
-rw-r--r--static/default.css4
-rw-r--r--templates/charts.tmpl12
-rw-r--r--templates/site.tmpl5
-rw-r--r--time_buckets.go140
-rw-r--r--user_agent.go54
-rw-r--r--user_agent_set.go52
-rw-r--r--utils.go12
16 files changed, 483 insertions, 191 deletions
diff --git a/charts.go b/charts.go
index b90f347..8bb1e64 100644
--- a/charts.go
+++ b/charts.go
@@ -33,10 +33,10 @@ type TimeCountable interface {
type chart struct {
width, height int
- data TimeBuckets
+ data HitSet
}
-func NewChart(data TimeBuckets, opts ...ChartOption) (*chart, error) {
+func NewChart(data HitSet, opts ...ChartOption) (*chart, error) {
out := &chart{data: data}
for _, o := range opts {
if err := o(out); err != nil {
@@ -59,7 +59,7 @@ func Dimentions(height, width int) ChartOption {
}
}
-func SparklineSVG(w io.Writer, data TimeBuckets, d time.Duration) error {
+func SparklineSVG(w io.Writer, data *HitSet, d time.Duration) error {
hits := gochart.TimeSeries{
//Name: "Hits",
Style: gochart.Style{
@@ -98,7 +98,7 @@ func SparklineSVG(w io.Writer, data TimeBuckets, d time.Duration) error {
return nil
}
-func HitChartSVG(w io.Writer, data TimeBuckets, d time.Duration) error {
+func HitChartSVG(w io.Writer, data *HitSet, d time.Duration) error {
hits := gochart.TimeSeries{
Name: "Hits",
Style: gochart.Style{
diff --git a/cmd/server/charts.go b/cmd/server/charts.go
index 10a042b..7c7ab9d 100644
--- a/cmd/server/charts.go
+++ b/cmd/server/charts.go
@@ -45,7 +45,7 @@ func sparklineHandler(db sws.HitStore) http.HandlerFunc {
return
}
debug("retrieved", len(hits), "hits")
- data := sws.HitsToTimeBuckets(hits, time.Minute)
+ data := sws.NewHitSet(hits, begin, end, time.Minute)
// Ensure the buckets start and end at the right time
data.Fill(&begin, &end)
@@ -96,7 +96,7 @@ func svgChartHandler(db sws.HitStore) http.HandlerFunc {
}
debug("retrieved", len(hits), "hits")
- data := sws.HitsToTimeBuckets(hits, time.Minute)
+ data := sws.NewHitSet(hits, begin, end, time.Minute)
data.Fill(&begin, &end)
w.Header().Set("Content-Type", "image/svg+xml")
diff --git a/cmd/server/sites.go b/cmd/server/sites.go
index efc447b..25453ef 100644
--- a/cmd/server/sites.go
+++ b/cmd/server/sites.go
@@ -1,6 +1,7 @@
package main
import (
+ "fmt"
"net/http"
"time"
@@ -49,22 +50,23 @@ func handleSite(db sws.SiteStore, rndr Renderer) http.HandlerFunc {
return
}
- pages := sws.PagesFromHits(hits)
- userAgents := sws.UserAgentsFromHits(hits)
-
- buckets := sws.HitsToTimeBuckets(hits, time.Hour)
- buckets.Fill(begin, end)
+ hitSet := sws.NewHitSet(hits, *begin, *end, time.Hour)
+ fmt.Printf("site begin: %s end: %s\n", *begin, *end)
+ hitSet.Fill(begin, end)
+ fmt.Printf("hitset begin: %s end: %s\n", hitSet.Begin(), hitSet.End())
+ pageSet := sws.NewPageSet(hitSet)
+ uaSet := sws.NewUserAgentSet(hitSet)
payload := struct {
Site *sws.Site
- Pages map[string]*sws.Page
- UserAgents sws.UserAgentSummary
- Hits sws.TimeBuckets
+ Pages sws.PageSet
+ UserAgents sws.UserAgentSet
+ Hits *sws.HitSet
}{
Site: site,
- Pages: pages,
- UserAgents: userAgents,
- Hits: buckets,
+ Pages: pageSet,
+ UserAgents: uaSet,
+ Hits: hitSet,
}
if err := rndr.Render(w, "site", payload); err != nil {
httpError(w, 500, err.Error())
diff --git a/counter/sws.js b/counter/sws.js
index 6995bbb..56a283e 100644
--- a/counter/sws.js
+++ b/counter/sws.js
@@ -1,27 +1,28 @@
var d = document
-var de = document.documentElement
+var de = d.documentElement
var w = window
var l = d.location
var n = w.navigator
var esc = encodeURIComponent
-var me = document.currentScript
-console.log('me:', me)
-console.log('me.sws:', me.dataset.sws)
+var me = d.currentScript
-var _sws = w._sws || {noxhr: false, noauto: false}
-console.log('_sws:', _sws)
-_sws.d = _sws.d || me.dataset.sws || 'http://sws.userspace.com.au/sws.gif'
+var _sws = w._sws || {noauto: false}
+_sws.d = _sws.d || me.src
_sws.site = _sws.site || me.dataset.site
-console.log('using', _sws.d)
function count (p, obj) {
- console.log('sending', p, JSON.stringify(obj))
+ if (l.hostname.match(/(localhost$|^127\.|^10\.|^172\.16\.|^192\.168\.)/))
+ return
+ if ('visibilityState' in d && d.visibilityState === 'prerender')
+ return
+
var qs = Object.keys(obj)
.map(function (k) {
return esc(k) + '=' + esc(obj[k])
})
.join('&')
- if (!_sws.xhr) {
+
+ if (!_sws.noxhr) {
var r = new w.XMLHttpRequest()
r.open('GET', p + '?' + qs, true)
r.send()
@@ -32,9 +33,7 @@ function count (p, obj) {
}
function ready (fn) {
- if (d.attachEvent
- ? d.readyState === 'complete'
- : d.readyState !== 'loading') {
+ if (d.attachEvent ? d.readyState === 'complete' : d.readyState !== 'loading') {
fn()
} else {
d.addEventListener('DOMContentLoaded', fn)
@@ -46,7 +45,8 @@ var viewPort = (w.innerWidth || de.clientWidth || d.body.clientWidth) + 'x' +
ready(function () {
if (!_sws.noauto) {
- count(_sws.d, {
+ var ep = new URL(_sws.d)
+ count(ep.protocol+'//'+ep.host+'/sws.gif', {
i: _sws.site,
s: l.protocol,
h: l.host,
diff --git a/counter/test.html b/counter/test.html
deleted file mode 100644
index ef2f621..0000000
--- a/counter/test.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!doctype html>
-<html>
- <head>
- <meta charset="utf-8">
- <script>
- var _sws = {
- title: "test title"
- }
- </script>
- <script async src="http://localhost:5000/sws.js"></script>
- <title>This is the title</title>
- <noscript>
- <img src="http://localhost:5000/sws.gif?s=http%3A&h=localhost%3A5000&p=%2F?noscript&r=&u=Mozilla%2F5.0%20(Windows%20NT%2010.0%3B%20Win64%3B%20x64)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F69.0.3497.32%20Safari%2F537.36&v=1916x563" />
- </noscript>
- </head>
- <body>
- <a href="?referred">test</a>
- </body>
-</html>
diff --git a/hit.go b/hit.go
index c1993c5..e227f39 100644
--- a/hit.go
+++ b/hit.go
@@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "sort"
"strconv"
"strings"
"time"
@@ -32,6 +33,13 @@ type Hit struct {
UserAgent *UserAgent `db:"ua"`
}
+type Hitter interface {
+ Hits() []*Hit
+ Begin() time.Time
+ End() time.Time
+ Duration() time.Duration
+}
+
func (h Hit) String() string {
var out strings.Builder
for _, sp := range []*string{&h.Scheme, &h.Host, &h.Path, h.Query} {
@@ -42,6 +50,13 @@ func (h Hit) String() string {
return out.String()
}
+// SortHits in ascending order by time.
+func SortHits(hits []*Hit) {
+ sort.Slice(hits, func(i, j int) bool {
+ return hits[i].CreatedAt.Before(hits[j].CreatedAt)
+ })
+}
+
func HitFromRequest(r *http.Request) (*Hit, error) {
out := &Hit{
CreatedAt: time.Now(),
diff --git a/hit_set.go b/hit_set.go
new file mode 100644
index 0000000..f12e236
--- /dev/null
+++ b/hit_set.go
@@ -0,0 +1,194 @@
+package sws
+
+import (
+ "sort"
+ "time"
+)
+
+type HitSet struct {
+ duration time.Duration
+ tMin, tMax time.Time
+ cMin, cMax int
+ hits []*Hit
+ buckets []*bucket
+}
+
+type bucket struct {
+ t time.Time
+ hits []*Hit
+}
+
+// NewHitSet converts a slice of hits to time buckets, group by duration.
+func NewHitSet(hits []*Hit, b, e time.Time, d time.Duration) *HitSet {
+ out := &HitSet{
+ duration: d,
+ buckets: make([]*bucket, 0),
+ }
+ for _, h := range hits {
+ out.Add(h)
+ }
+ //out.updateMinMax()
+ sort.Sort(out)
+ return out
+}
+
+// Hits returns all hits in the set.
+func (hs *HitSet) Hits() []*Hit {
+ out := make([]*Hit, 0)
+ for _, b := range hs.buckets {
+ for _, h := range b.hits {
+ out = append(out, h)
+ }
+ }
+ SortHits(out)
+ return out
+}
+
+func (hs *HitSet) Add(h *Hit) {
+ k := h.CreatedAt.Truncate(hs.duration)
+ if k.Before(hs.tMin) {
+ hs.tMin = k
+ }
+ if k.After(hs.tMax) {
+ hs.tMax = k
+ }
+
+ var bk *bucket
+ for _, b := range hs.buckets {
+ if b.t.Equal(k) {
+ bk = b
+ }
+ }
+ if bk == nil {
+ // Create new bucket
+ bk = &bucket{t: k, hits: []*Hit{h}}
+ hs.buckets = append(hs.buckets, bk)
+ }
+ c := len(bk.hits)
+ if c < hs.cMin {
+ hs.cMin = c
+ }
+ if c > hs.cMax {
+ hs.cMax = c
+ }
+}
+
+// Implement Hitter interface.
+func (hs *HitSet) Begin() time.Time {
+ return hs.tMin
+}
+
+func (hs *HitSet) End() time.Time {
+ return hs.tMax
+}
+
+func (hs *HitSet) Duration() time.Duration {
+ return hs.duration
+}
+
+func (hs HitSet) Count() int {
+ out := 0
+ for _, b := range hs.buckets {
+ out += len(b.hits)
+ }
+ return out
+}
+
+// Implement sort.Interface
+func (hs HitSet) Len() int { return len(hs.buckets) }
+func (hs HitSet) Less(i, j int) bool { return hs.buckets[i].t.Before(hs.buckets[i].t) }
+func (hs HitSet) Swap(i, j int) { hs.buckets[i], hs.buckets[j] = hs.buckets[j], hs.buckets[i] }
+
+func (hs *HitSet) Fill(b, e *time.Time) {
+ begin := hs.tMin
+ if b != nil {
+ begin = *b
+ }
+ end := hs.tMax
+ if e != nil {
+ end = *e
+ }
+
+ total := diffDurations(begin, end, hs.duration)
+
+ newBuckets := make([]*bucket, total)
+
+ var existing int
+ var idx int
+ for n := begin; idx < total && !n.After(end); n = n.Add(hs.duration) {
+ switch {
+ case existing >= len(hs.buckets):
+ newBuckets[idx] = &bucket{t: n, hits: []*Hit{}}
+
+ case n.Before(hs.buckets[existing].t):
+ newBuckets[idx] = &bucket{t: n, hits: []*Hit{}}
+
+ default:
+ newBuckets[idx] = hs.buckets[existing]
+ existing++
+ }
+ idx++
+ }
+ hs.tMin = begin
+ hs.tMax = end
+ //hs.updateMinMax()
+ hs.buckets = newBuckets
+}
+
+// XYValues splits the buckets into two data series, one with the times
+// and the other with the values.
+func (hs HitSet) XYValues() ([]time.Time, []float64) {
+ x := make([]time.Time, len(hs.buckets))
+ y := make([]float64, len(hs.buckets))
+ for i, b := range hs.buckets {
+ x[i] = b.t
+ y[i] = float64(len(b.hits))
+ }
+ return x, y
+}
+
+func (hs HitSet) YMax() int {
+ return hs.cMax
+}
+func (hs HitSet) XSeries() []*bucket {
+ return hs.buckets
+}
+
+func (b bucket) Label() string {
+ return b.t.Format("15:04 Jan 2")
+}
+
+func (b bucket) YValue() int {
+ return len(b.hits)
+}
+
+func (b bucket) Time() time.Time {
+ return b.t
+}
+
+/*
+func (hs *HitSet) updateMinMax() {
+ if len(hs.buckets) < 1 {
+ return
+ }
+ hs.cMin = len(hs.buckets[0].hits)
+ hs.cMax = len(hs.buckets[0].hits)
+ hs.tMin = hs.buckets[0].t
+ hs.tMax = hs.buckets[0].t
+ for _, b := range hs.buckets {
+ c := len(b.hits)
+ if c < hs.cMin {
+ hs.cMin = c
+ }
+ if c > hs.cMax {
+ hs.cMax = c
+ }
+ if b.t.Before(hs.tMin) {
+ hs.tMin = b.t
+ }
+ if b.t.After(hs.tMax) {
+ hs.tMax = b.t
+ }
+ }
+}
+*/
diff --git a/page.go b/page.go
index e9ab785..93aecab 100644
--- a/page.go
+++ b/page.go
@@ -1,6 +1,8 @@
package sws
import (
+ "fmt"
+ "sort"
"time"
)
@@ -8,30 +10,30 @@ type Page struct {
SiteID int `json:"site_id"`
Path string `json:"path"`
Title *string `json:"title,omitempty"`
- Count int `json:"count"`
LastVisitedAt time.Time `json:"last_visited_at"`
-
+ hitSet *HitSet
// TODO
Site *Site `json:"-"`
}
-func PagesFromHits(hits []*Hit) map[string]*Page {
- out := make(map[string]*Page)
- for _, h := range hits {
- p, ok := out[h.Path]
- if !ok {
- p = &Page{
- Path: h.Path,
- SiteID: *h.SiteID,
- Title: h.Title,
- LastVisitedAt: h.CreatedAt,
- }
- }
- if p.LastVisitedAt.Before(h.CreatedAt) {
- p.LastVisitedAt = h.CreatedAt
- }
- p.Count++
- out[h.Path] = p
- }
- return out
+func (p Page) YMax() int {
+ return p.hitSet.YMax()
+}
+func (p Page) XSeries() []*bucket {
+ p.hitSet.Fill(nil, nil)
+ sort.Sort(p.hitSet)
+ fmt.Printf("page begin: %s end: %s\n", p.hitSet.Begin(), p.hitSet.End())
+ return p.hitSet.XSeries()
+}
+
+func (p Page) Count() int {
+ return p.hitSet.Count()
+}
+
+func (p Page) Label() string {
+ return p.Path
+}
+
+func (p Page) YValue() int {
+ return p.hitSet.Count()
}
diff --git a/page_set.go b/page_set.go
new file mode 100644
index 0000000..46c2964
--- /dev/null
+++ b/page_set.go
@@ -0,0 +1,59 @@
+package sws
+
+import "fmt"
+
+type PageSet map[string]*Page
+
+func NewPageSet(hitter Hitter) PageSet {
+ out := make(map[string]*Page)
+ for _, h := range hitter.Hits() {
+ p, ok := out[h.Path]
+ if !ok {
+ p = &Page{
+ Path: h.Path,
+ SiteID: *h.SiteID,
+ Title: h.Title,
+ LastVisitedAt: h.CreatedAt,
+ hitSet: &HitSet{
+ duration: hitter.Duration(),
+ },
+ }
+ }
+ if p.LastVisitedAt.Before(h.CreatedAt) {
+ p.LastVisitedAt = h.CreatedAt
+ }
+ p.hitSet.Add(h)
+ out[h.Path] = p
+ }
+ b := hitter.Begin()
+ e := hitter.End()
+ fmt.Printf("new pageset begin: %s end: %s\n", b, e)
+ for _, p := range out {
+ p.hitSet.Fill(&b, &e)
+ }
+ return PageSet(out)
+}
+
+func (ps PageSet) Page(p string) *Page {
+ pg, _ := ps[p]
+ return pg
+}
+
+func (ps PageSet) YMax() int {
+ max := 0
+ for _, p := range ps {
+ if p.Count() > max {
+ max = p.Count()
+ }
+ }
+ return max
+}
+func (ps PageSet) XSeries() []*Page {
+ out := make([]*Page, len(ps))
+ i := 0
+ for _, v := range ps {
+ out[i] = v
+ i++
+ }
+ return out
+}
diff --git a/static/default.css b/static/default.css
index aa1b8f6..c0c97d7 100644
--- a/static/default.css
+++ b/static/default.css
@@ -70,6 +70,10 @@ main {
position: absolute;
right: 0;
}
+.chart .bar {
+ margin-left: 2px;
+ margin-right: 2px;
+}
.chart .slot:hover .bar {
background: #88b6ff;
}
diff --git a/templates/charts.tmpl b/templates/charts.tmpl
index d5d6db0..8d3bba1 100644
--- a/templates/charts.tmpl
+++ b/templates/charts.tmpl
@@ -1,16 +1,16 @@
{{ define "timeBarChart" }}
- <ul class="chart bar">
- {{ $max := .CountMax }}
- {{ range .Data }}
- <li class="slot{{ if eq .Time.Hour 0 }} midnight{{ end }}" data-x="{{ .Time|timeHour }}" data-y="{{ .Count }}" data-percent="{{ percent .Count $max }}">
- <div class="bar" style="height:{{ percent .Count $max }}%" />
+ <ul class="chart bar time">
+ {{ $max := .YMax }}
+ {{ range .XSeries }}
+ <li class="slot{{ if eq .Time.Hour 0 }} midnight{{ end }}" data-x="{{ .Label }}" data-y="{{ .YValue }}" data-percent="{{ percent .YValue $max }}">
+ <div class="bar" style="height:{{ percent .YValue $max }}%" />
</li>
{{ end }}
</ul>
{{ end }}
{{ define "barChart" }}
- <ul class="chart bar">
+ <ul class="chart bar category">
{{ $max := .YMax }}
{{ range .XSeries }}
<li class="slot" data-x="{{ .Label }}" data-y="{{ .YValue }}" data-percent="{{ percent .YValue $max }}">
diff --git a/templates/site.tmpl b/templates/site.tmpl
index 5562dd4..44e0777 100644
--- a/templates/site.tmpl
+++ b/templates/site.tmpl
@@ -11,8 +11,13 @@
</fig>
<h2>Popular pages</h2>
<ul class="pages">
+ {{ $pages := .Pages }}
{{ range .Pages }}
{{ template "pageForList" . }}
+ <fig>
+ {{ $pathHits := $pages.Page .Path }}
+ {{ template "timeBarChart" $pathHits }}
+ </fig>
{{ end }}
</ul>
<h2>User Agents</h2>
diff --git a/time_buckets.go b/time_buckets.go
index ebb25e6..f9748d2 100644
--- a/time_buckets.go
+++ b/time_buckets.go
@@ -1,63 +1,59 @@
+// +build ignore
+
package sws
import (
- "fmt"
"sort"
"time"
)
type TimeBuckets struct {
- Duration time.Duration
+ duration time.Duration
TimeMin, TimeMax time.Time
CountMin, CountMax int
- Data []Bucket
+ buckets []Bucket
}
type Bucket struct {
- Time time.Time
- Count int
-}
-
-/*
-func (tb TimeBuckets) YMax() int {
- return tb.CountMax
-}
-
-func (tb TimeBuckets) Next() TimeCountable {
- return tb.Data
+ t time.Time
+ hits []*Hit
}
-func (b Bucket) XValue() time.Time {
- return b.Time
+func (tb TimeBuckets) Hits() []*Hit {
+ out := make([]*Hit, 0)
+ for _, b := range tb.buckets {
+ for _, h := range b.hits {
+ out = append(out, h)
+ }
+ }
+ SortHits(out)
+ return out
}
-func (b Bucket) YValue() int {
- return b.Count
-}
-*/
+// Implement sort.Interface
+func (tb TimeBuckets) Len() int { return len(tb.buckets) }
+func (tb TimeBuckets) Less(i, j int) bool { return tb.buckets[i].t.Before(tb.buckets[i].t) }
+func (tb TimeBuckets) Swap(i, j int) { tb.buckets[i], tb.buckets[j] = tb.buckets[j], tb.buckets[i] }
// XYValues splits the buckets into two data series, one with the times
// and the other with the values.
func (tb TimeBuckets) XYValues() ([]time.Time, []float64) {
- x := make([]time.Time, len(tb.Data))
- y := make([]float64, len(tb.Data))
- for i, b := range tb.Data {
- x[i] = b.Time
- y[i] = float64(b.Count)
+ x := make([]time.Time, len(tb.buckets))
+ y := make([]float64, len(tb.buckets))
+ for i, b := range tb.buckets {
+ x[i] = b.t
+ y[i] = float64(len(b.hits))
}
return x, y
}
-func (b Bucket) String() string {
- return fmt.Sprintf("%s => %d", b.Time, b.Count)
-}
-
// HitsToTimeBuckets converts a slice of hits to time buckets, group by durtation.
func HitsToTimeBuckets(hits []*Hit, d time.Duration) TimeBuckets {
out := TimeBuckets{
- Duration: d,
- Data: make([]Bucket, 0),
+ duration: d,
+ buckets: make([]Bucket, 0),
}
+ SortHits(hits)
for j, h := range hits {
k := h.CreatedAt.Truncate(d)
if j == 0 || k.Before(out.TimeMin) {
@@ -67,28 +63,21 @@ func HitsToTimeBuckets(hits []*Hit, d time.Duration) TimeBuckets {
out.TimeMax = k
}
var found bool
- for i, tb := range out.Data {
- if tb.Time.Equal(k) {
- out.Data[i].Count++
+ for i, tb := range out.buckets {
+ if tb.t.Equal(k) {
+ out.buckets[i].hits = append(out.buckets[i].hits, h)
found = true
}
}
if !found {
- out.Data = append(out.Data, Bucket{Time: k, Count: 1})
+ out.buckets = append(out.buckets, Bucket{t: k, hits: []*Hit{h}})
}
}
- out.Sort()
out.updateMinMax()
+ sort.Sort(out)
return out
}
-// Sort order the buckets in ascending order by time.
-func (tb *TimeBuckets) Sort() {
- sort.Slice(tb.Data, func(i, j int) bool {
- return tb.Data[i].Time.Before(tb.Data[j].Time)
- })
-}
-
// Fill adds extra buckets so each duration segment has a bucket.
// If no begin or end times are provided it uses the existing min and max times.
func (tb *TimeBuckets) Fill(b, e *time.Time) {
@@ -101,51 +90,70 @@ func (tb *TimeBuckets) Fill(b, e *time.Time) {
end = *e
}
- total := diffDurations(begin, end, tb.Duration)
- tb.Sort()
+ total := diffDurations(begin, end, tb.duration)
newBuckets := make([]Bucket, total)
var existing int
var idx int
- for n := begin; idx < total && !n.After(end); n = n.Add(tb.Duration) {
+ for n := begin; idx < total && !n.After(end); n = n.Add(tb.duration) {
switch {
- case existing >= len(tb.Data):
- newBuckets[idx] = Bucket{Time: n, Count: 0}
+ case existing >= len(tb.buckets):
+ newBuckets[idx] = Bucket{t: n, hits: []*Hit{}}
- case n.Before(tb.Data[existing].Time):
- newBuckets[idx] = Bucket{Time: n, Count: 0}
+ case n.Before(tb.buckets[existing].t):
+ newBuckets[idx] = Bucket{t: n, hits: []*Hit{}}
default:
- newBuckets[idx] = tb.Data[existing]
+ newBuckets[idx] = tb.buckets[existing]
existing++
}
idx++
}
tb.updateMinMax()
- tb.Data = newBuckets
+ tb.buckets = newBuckets
+}
+
+func (tb TimeBuckets) YMax() int {
+ return tb.CountMax
+}
+func (tb TimeBuckets) XSeries() []Bucket {
+ return tb.buckets
+}
+
+func (b Bucket) Label() string {
+ return b.t.Format("15:04 Jan 2")
+}
+
+func (b Bucket) Time() string {
+ return b.t.Format("15:04 Jan 2")
+}
+
+func (b Bucket) YValue() int {
+ return len(b.hits)
}
func (tb *TimeBuckets) updateMinMax() {
- if len(tb.Data) < 1 {
+ if len(tb.buckets) < 1 {
return
}
- minC := tb.Data[0].Count
- maxC := tb.Data[0].Count
- minT := tb.Data[0].Time
- maxT := tb.Data[0].Time
- for _, b := range tb.Data {
- if b.Count < minC {
- minC = b.Count
+ minC := len(tb.buckets[0].hits)
+ maxC := len(tb.buckets[0].hits)
+ minT := tb.buckets[0].t
+ maxT := tb.buckets[0].t
+ for _, b := range tb.buckets {
+ c := len(b.hits)
+ if c < minC {
+ minC = c
}
- if b.Count > maxC {
- maxC = b.Count
+ if c > maxC {
+ maxC = c
}
- if b.Time.Before(minT) {
- minT = b.Time
+ if b.t.Before(minT) {
+ minT = b.t
}
- if b.Time.After(maxT) {
- maxT = b.Time
+ if b.t.After(maxT) {
+ maxT = b.t
}
}
tb.TimeMin = minT
diff --git a/user_agent.go b/user_agent.go
index dbfd1bf..6b16466 100644
--- a/user_agent.go
+++ b/user_agent.go
@@ -16,9 +16,8 @@ type UserAgent struct {
Hash string `json:"hash"`
Name string `json:"name"`
LastSeenAt time.Time `json:"last_seen_at" db:"last_seen_at"`
- Count int
-
- ua *detector.UserAgent
+ hitSet *HitSet
+ ua *detector.UserAgent
}
var (
@@ -53,57 +52,16 @@ func UserAgentFromRequest(r *http.Request) (*UserAgent, error) {
}, nil
}
-// UserAgentsFromHits collects the browsers from provided hits.
-func UserAgentsFromHits(hits []*Hit) UserAgentSummary {
- out := make(map[string]*UserAgent)
- for _, h := range hits {
- if h.UserAgentHash == nil {
- continue
- }
- b, ok := out[*h.UserAgentHash]
- if !ok {
- b = &UserAgent{
- Name: h.UserAgent.Name,
- LastSeenAt: h.CreatedAt,
- ua: detector.New(h.UserAgent.Name),
- }
- }
- if b.LastSeenAt.Before(h.CreatedAt) {
- b.LastSeenAt = h.CreatedAt
- }
- b.Count++
- out[*h.UserAgentHash] = b
- }
- return UserAgentSummary(out)
-}
-
-type UserAgentSummary map[string]*UserAgent
-
-func (s UserAgentSummary) YMax() int {
- max := 0
- for _, v := range s {
- if v.Count > max {
- max = v.Count
- }
- }
- return max
-}
-func (s UserAgentSummary) XSeries() []*UserAgent {
- out := make([]*UserAgent, len(s))
- i := 0
- for _, v := range s {
- out[i] = v
- i++
- }
- return out
+func (ua UserAgent) Count() int {
+ return ua.hitSet.Count()
}
func (ua UserAgent) Label() string {
- return ua.Browser()
+ return ua.Browser() + "/" + ua.BrowserVersion()
}
func (ua UserAgent) YValue() int {
- return ua.Count
+ return ua.Count()
}
func (ua UserAgent) IsBot() bool {
diff --git a/user_agent_set.go b/user_agent_set.go
new file mode 100644
index 0000000..2504841
--- /dev/null
+++ b/user_agent_set.go
@@ -0,0 +1,52 @@
+package sws
+
+import (
+ detector "github.com/mssola/user_agent"
+)
+
+type UserAgentSet map[string]*UserAgent
+
+// NewUserAgentSet collects the browsers from provided hits.
+func NewUserAgentSet(hitter Hitter) UserAgentSet {
+ out := make(map[string]*UserAgent)
+ for _, h := range hitter.Hits() {
+ if h.UserAgentHash == nil {
+ // TODO
+ continue
+ }
+ b, ok := out[*h.UserAgentHash]
+ if !ok {
+ b = &UserAgent{
+ Name: h.UserAgent.Name,
+ LastSeenAt: h.CreatedAt,
+ hitSet: &HitSet{},
+ ua: detector.New(h.UserAgent.Name),
+ }
+ }
+ if b.LastSeenAt.Before(h.CreatedAt) {
+ b.LastSeenAt = h.CreatedAt
+ }
+ b.hitSet.Add(h)
+ out[*h.UserAgentHash] = b
+ }
+ return UserAgentSet(out)
+}
+
+func (uas UserAgentSet) YMax() int {
+ max := 0
+ for _, ua := range uas {
+ if ua.Count() > max {
+ max = ua.Count()
+ }
+ }
+ return max
+}
+func (s UserAgentSet) XSeries() []*UserAgent {
+ out := make([]*UserAgent, len(s))
+ i := 0
+ for _, v := range s {
+ out[i] = v
+ i++
+ }
+ return out
+}
diff --git a/utils.go b/utils.go
index c31c4b3..a51279e 100644
--- a/utils.go
+++ b/utils.go
@@ -27,3 +27,15 @@ func hashID(salt string, id int) string {
out, _ := h.Encode([]int{id})
return out
}
+
+func diffDurations(t1, t2 time.Time, d time.Duration) int {
+ t1n := t1.Unix()
+ t2n := t2.Unix()
+ var diff int64
+ if t1n > t2n {
+ diff = t1n - t2n
+ } else {
+ diff = t2n - t1n
+ }
+ return int(float64(diff) / d.Seconds())
+}