carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package analytics
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "math"
8 "os"
9 "sort"
10 "strconv"
11 "strings"
12 "time"
13
14 "github.com/carlosframework/platform/internal/release"
15 "github.com/carlosframework/platform/internal/usage"
16)
17
18// DaySummary is one UTC day across every box.
19type DaySummary struct {
20 Date string `json:"date"`
21 PV int64 `json:"pv"`
22 Bots int64 `json:"bots"`
23 Bytes int64 `json:"bytes"`
24 Visitors int64 `json:"visitors"`
25 Hours [24]int64 `json:"hours"`
26 Restarted bool `json:"restarted"`
27}
28
29// Summary is what the tab and the API render: per-day rows plus totals.
30// Visitors is the SUM of daily estimates (spec §3.3: "daily visitors,
31// summed"), never a cross-day union.
32type Summary struct {
33 From, To string
34 Days []DaySummary
35 PV, Bots int64
36 Bytes int64
37 Visitors int64
38 Pages map[string]int64
39 Referers map[string]int64
40 Events map[string]int64
41 Countries map[string]int64
42 GeoSources map[string]int64
43 PagesApprox bool
44 ReferersApprox bool
45 EventsApprox bool
46 Dropped Dropped
47 Boxes []string
48}
49
50// Dates lists the UTC dates from from to to inclusive.
51func Dates(from, to time.Time) []string {
52 var out []string
53 for d := from.UTC().Truncate(24 * time.Hour); !d.After(to.UTC()); d = d.Add(24 * time.Hour) {
54 out = append(out, usage.DayOf(d))
55 }
56 return out
57}
58
59// ReadRange lists the app's boxes and reads each day file in the range,
60// merging across boxes: counters sum, visitor sketches union per day,
61// top-key summaries merge and re-fold. A missing day file is an empty
62// day; a store error fails the read.
63func ReadRange(ctx context.Context, s release.Store, sqid, app string, from, to time.Time) (Summary, error) {
64 sum := Summary{From: usage.DayOf(from), To: usage.DayOf(to)}
65 prefix, err := Prefix(sqid, app)
66 if err != nil {
67 return sum, err
68 }
69 boxDirs, err := s.ListDirs(ctx, prefix)
70 if err != nil {
71 return sum, err
72 }
73 for _, dir := range boxDirs {
74 sum.Boxes = append(sum.Boxes, boxName(dir))
75 }
76 sort.Strings(sum.Boxes)
77 pages, refs, events := NewTopK(), NewTopK(), NewTopK()
78 countries, sources := map[string]int64{}, map[string]int64{}
79 for _, date := range Dates(from, to) {
80 ds := DaySummary{Date: date}
81 vis := &HLL{}
82 for _, box := range sum.Boxes {
83 key, err := DayKey(sqid, app, box, date)
84 if err != nil {
85 return sum, err
86 }
87 raw, _, err := s.Get(ctx, key)
88 if errors.Is(err, os.ErrNotExist) {
89 continue
90 }
91 if err != nil {
92 return sum, err
93 }
94 var day Day
95 if err := json.Unmarshal(raw, &day); err != nil {
96 return sum, err
97 }
98 for h, row := range day.Hours {
99 if i, err := strconv.Atoi(h); err == nil && i >= 0 && i < 24 {
100 ds.Hours[i] += row.PV
101 }
102 ds.PV += row.PV
103 ds.Bots += row.Bots
104 ds.Bytes += row.Bytes
105 }
106 if v, err := DecodeHLL(day.Visitors); err == nil {
107 vis.Merge(v)
108 }
109 pages.Merge(TopKFrom(day.Pages))
110 refs.Merge(TopKFrom(day.Referers))
111 events.Merge(TopKFrom(day.Events))
112 mergeCounts(countries, day.Countries, maxCountryKeys, nil)
113 mergeCounts(sources, day.GeoSources, maxGeoSourceKeys, attributed)
114 sum.Dropped.E = addDrops(sum.Dropped.E, day.Dropped.E)
115 sum.Dropped.A = addDrops(sum.Dropped.A, day.Dropped.A)
116 sum.Dropped.Flush += day.Dropped.Flush
117 ds.Restarted = ds.Restarted || day.Restarted
118 }
119 if !vis.Empty() {
120 ds.Visitors = int64(math.Round(vis.Count()))
121 }
122 sum.Days = append(sum.Days, ds)
123 sum.PV += ds.PV
124 sum.Bots += ds.Bots
125 sum.Bytes += ds.Bytes
126 sum.Visitors += ds.Visitors
127 }
128 sum.Pages, sum.PagesApprox = pages.Fold(), pages.Approximate()
129 sum.Referers, sum.ReferersApprox = refs.Fold(), refs.Approximate()
130 sum.Events, sum.EventsApprox = events.Fold(), events.Approximate()
131 sum.Countries, sum.GeoSources = countries, sources
132 return sum, nil
133}
134
135func boxName(dir string) string {
136 t := strings.TrimSuffix(dir, "/")
137 if i := strings.LastIndexByte(t, '/'); i >= 0 {
138 return t[i+1:]
139 }
140 return t
141}
142