diff options
| author | Felix Hanley <felix@userspace.com.au> | 2020-02-21 08:27:42 +0000 |
|---|---|---|
| committer | Felix Hanley <felix@userspace.com.au> | 2020-02-21 08:27:42 +0000 |
| commit | b25a204138d82d0b8c29eaf81003cc6b5553d58f (patch) | |
| tree | 4b4bebcf5f8ce807a463ab97cb7768da07b5aee5 | |
| parent | d68c6aad01c2f6e7f30389536152e1f96682b5cd (diff) | |
| download | sws-b25a204138d82d0b8c29eaf81003cc6b5553d58f.tar.gz sws-b25a204138d82d0b8c29eaf81003cc6b5553d58f.tar.bz2 | |
Add HitSet and associated interfaces for charts and reuse
| -rw-r--r-- | charts.go | 8 | ||||
| -rw-r--r-- | cmd/server/charts.go | 4 | ||||
| -rw-r--r-- | cmd/server/sites.go | 24 | ||||
| -rw-r--r-- | counter/sws.js | 28 | ||||
| -rw-r--r-- | counter/test.html | 19 | ||||
| -rw-r--r-- | hit.go | 15 | ||||
| -rw-r--r-- | hit_set.go | 194 | ||||
| -rw-r--r-- | page.go | 44 | ||||
| -rw-r--r-- | page_set.go | 59 | ||||
| -rw-r--r-- | static/default.css | 4 | ||||
| -rw-r--r-- | templates/charts.tmpl | 12 | ||||
| -rw-r--r-- | templates/site.tmpl | 5 | ||||
| -rw-r--r-- | time_buckets.go | 140 | ||||
| -rw-r--r-- | user_agent.go | 54 | ||||
| -rw-r--r-- | user_agent_set.go | 52 | ||||
| -rw-r--r-- | utils.go | 12 |
16 files changed, 483 insertions, 191 deletions
@@ -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> @@ -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 + } + } +} +*/ @@ -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 +} @@ -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()) +} |
