Skip to content

Commit b4126b7

Browse files
committed
Fix charts for SQLite
This fixes a regression introduces in 6bc0162; that changed so that stat rows were no longer stored if there are no visits to store. That works fine, except for hit_stats in SQLite since it will do a "select", "delete", merges with new pageviews, and then an insert. But if there are no new visits (only pageviews) it will never re-insert the rows. So check the full array instead, and write a migration to correct the wrong data.
1 parent 0773273 commit b4126b7

File tree

5 files changed

+177
-5
lines changed

5 files changed

+177
-5
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@
2121
# Coverage reports
2222
/coverage
2323
/coverage.*
24+
25+
# Dev log
26+
/.dev.log

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ This list is not comprehensive, and only lists new features and major changes,
44
but not every minor bugfix. The goatcounter.com service generally runs the
55
latest master.
66

7+
2022-11-15 v2.4.1
8+
-----------------
9+
- Fix regression that caused the charts for SQLite to be off.
10+
711
2022-11-08 v2.4.0
812
-----------------
913
- Add a more fully-featured API that can also retrieve the dashboard statistics.

cron/hit_stat.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ import (
88
"context"
99
"strconv"
1010

11+
"golang.org/x/exp/slices"
1112
"zgo.at/errors"
1213
"zgo.at/goatcounter/v2"
1314
"zgo.at/zdb"
1415
"zgo.at/zstd/zjson"
1516
)
1617

18+
var empty = []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
19+
1720
func updateHitStats(ctx context.Context, hits []goatcounter.Hit) error {
1821
return errors.Wrap(zdb.TX(ctx, func(ctx context.Context) error {
1922
type gt struct {
2023
count []int
21-
total int
2224
day string
2325
hour string
2426
pathID int64
@@ -51,7 +53,6 @@ func updateHitStats(ctx context.Context, hits []goatcounter.Hit) error {
5153
hour, _ := strconv.ParseInt(h.CreatedAt.Format("15"), 10, 8)
5254
if h.FirstVisit {
5355
v.count[hour] += 1
54-
v.total += 1
5556
}
5657
grouped[k] = v
5758
}
@@ -79,9 +80,10 @@ func updateHitStats(ctx context.Context, hits []goatcounter.Hit) error {
7980
// }
8081

8182
for _, v := range grouped {
82-
if v.total > 0 {
83-
ins.Values(siteID, v.day, v.pathID, zjson.MustMarshal(v.count))
83+
if slices.Equal(v.count, empty) {
84+
continue
8485
}
86+
ins.Values(siteID, v.day, v.pathID, zjson.MustMarshal(v.count))
8587
}
8688
return errors.Wrap(ins.Finish(), "updateHitStats hit_stats")
8789
}), "cron.updateHitStats")
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright © Martin Tournoij – This file is part of GoatCounter and published
2+
// under the terms of a slightly modified EUPL v1.2 license, which can be found
3+
// in the LICENSE file or at https://license.goatcounter.com
4+
5+
package gomig
6+
7+
import (
8+
"context"
9+
"strconv"
10+
11+
"golang.org/x/exp/slices"
12+
"zgo.at/errors"
13+
"zgo.at/goatcounter/v2"
14+
"zgo.at/zdb"
15+
"zgo.at/zstd/zjson"
16+
)
17+
18+
func CorrectHitStats(ctx context.Context) error {
19+
// Only for SQLite
20+
if zdb.SQLDialect(ctx) == zdb.DialectPostgreSQL {
21+
return nil
22+
}
23+
24+
err := zdb.TX(goatcounter.NewCache(goatcounter.NewConfig(ctx)), func(ctx context.Context) error {
25+
err := zdb.Exec(ctx, `delete from hit_stats where day >= '2022-11-08'`)
26+
if err != nil {
27+
return err
28+
}
29+
30+
var sites goatcounter.Sites
31+
err = sites.UnscopedList(ctx)
32+
if err != nil {
33+
return err
34+
}
35+
36+
for _, s := range sites {
37+
var hits goatcounter.Hits
38+
err = zdb.Select(ctx, &hits, `select * from hits where
39+
created_at >= '2022-11-08 00:00:00' and first_visit=1 and bot in (0, 1)`)
40+
if err != nil {
41+
return err
42+
}
43+
44+
err = updateHitStats(goatcounter.WithSite(ctx, &s), hits)
45+
if err != nil {
46+
return err
47+
}
48+
}
49+
return nil
50+
})
51+
52+
if err == nil {
53+
err = zdb.Exec(ctx, `insert into version values ('2022-11-15-1-correct-hit-stats')`)
54+
}
55+
return err
56+
}
57+
58+
// below is a copy of cron/hit_stat.go
59+
60+
var empty = []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
61+
62+
func updateHitStats(ctx context.Context, hits []goatcounter.Hit) error {
63+
return errors.Wrap(zdb.TX(ctx, func(ctx context.Context) error {
64+
type gt struct {
65+
count []int
66+
day string
67+
hour string
68+
pathID int64
69+
}
70+
grouped := map[string]gt{}
71+
for _, h := range hits {
72+
if h.Bot > 0 {
73+
continue
74+
}
75+
76+
day := h.CreatedAt.Format("2006-01-02")
77+
dayHour := h.CreatedAt.Format("2006-01-02 15:00:00")
78+
k := day + strconv.FormatInt(h.PathID, 10)
79+
v := grouped[k]
80+
if len(v.count) == 0 {
81+
v.day = day
82+
v.hour = dayHour
83+
v.pathID = h.PathID
84+
v.count = make([]int, 24)
85+
86+
if zdb.SQLDialect(ctx) == zdb.DialectSQLite {
87+
var err error
88+
v.count, err = existingHitStats(ctx, h.Site, day, v.pathID)
89+
if err != nil {
90+
return err
91+
}
92+
}
93+
}
94+
95+
hour, _ := strconv.ParseInt(h.CreatedAt.Format("15"), 10, 8)
96+
if h.FirstVisit {
97+
v.count[hour] += 1
98+
}
99+
grouped[k] = v
100+
}
101+
102+
siteID := goatcounter.MustGetSite(ctx).ID
103+
ins := zdb.NewBulkInsert(ctx, "hit_stats", []string{"site_id", "day", "path_id", "stats"})
104+
if zdb.SQLDialect(ctx) == zdb.DialectPostgreSQL {
105+
ins.OnConflict(`on conflict on constraint "hit_stats#site_id#path_id#day" do update set
106+
stats = (
107+
with x as (
108+
select
109+
unnest(string_to_array(trim(hit_stats.stats, '[]'), ',')::int[]) as orig,
110+
unnest(string_to_array(trim(excluded.stats, '[]'), ',')::int[]) as new
111+
)
112+
select '[' || array_to_string(array_agg(orig + new), ',') || ']' from x
113+
) `)
114+
}
115+
// } else {
116+
// TODO: merge the arrays here and get rid of existingHitStats();
117+
// it's kinda tricky with SQLite :-/
118+
//
119+
// ins.OnConflict(`on conflict(site_id, path_id, day) do update set
120+
// stats = excluded.stats
121+
// `)
122+
// }
123+
124+
for _, v := range grouped {
125+
if slices.Equal(v.count, empty) {
126+
continue
127+
}
128+
ins.Values(siteID, v.day, v.pathID, zjson.MustMarshal(v.count))
129+
}
130+
return errors.Wrap(ins.Finish(), "updateHitStats hit_stats")
131+
}), "cron.updateHitStats")
132+
}
133+
134+
func existingHitStats(ctx context.Context, siteID int64, day string, pathID int64) ([]int, error) {
135+
var ex []struct {
136+
Stats []byte `db:"stats"`
137+
}
138+
err := zdb.Select(ctx, &ex, `/* existingHitStats */
139+
select stats from hit_stats
140+
where site_id=$1 and day=$2 and path_id=$3 limit 1`,
141+
siteID, day, pathID)
142+
if err != nil {
143+
return nil, errors.Wrap(err, "existingHitStats")
144+
}
145+
if len(ex) == 0 {
146+
return make([]int, 24), nil
147+
}
148+
149+
err = zdb.Exec(ctx, `delete from hit_stats where
150+
site_id=$1 and day=$2 and path_id=$3`,
151+
siteID, day, pathID)
152+
if err != nil {
153+
return nil, errors.Wrap(err, "delete")
154+
}
155+
156+
var ru []int
157+
if ex[0].Stats != nil {
158+
zjson.MustUnmarshal(ex[0].Stats, &ru)
159+
}
160+
161+
return ru, nil
162+
}

db/migrate/gomig/gomig.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ package gomig
77
import "context"
88

99
var Migrations = map[string]func(context.Context) error{
10-
"2021-12-08-1-set-chart-text": KeepAsText,
10+
"2021-12-08-1-set-chart-text": KeepAsText,
11+
"2022-11-15-1-correct-hit-stats": CorrectHitStats,
1112
}

0 commit comments

Comments
 (0)