Skip to content

Commit

Permalink
Merge pageviews
Browse files Browse the repository at this point in the history
Ability to merge pageviews from one path to the other.

Also add "case sensitive" option; useful if you want to delete "/Foo"
but not "/foo".

Fixes #145
Fixes #196
  • Loading branch information
arp242 committed Oct 18, 2022
1 parent 9f0d43f commit 02db82c
Show file tree
Hide file tree
Showing 20 changed files with 245 additions and 123 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ This list is not comprehensive, and only lists new features and major changes,
but not every minor bugfix. The goatcounter.com service generally runs the
latest master.

unreleased
----------
- Can now merge paths instead of just deleting them.

2022-10-17 v2.3.0
-----------------
- Expand campaigns: the `utm_campaign` or `campaign` parameter now is tracked
Expand Down
2 changes: 1 addition & 1 deletion cmd/goatcounter/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ func cmdDBQuery(f zli.Flags, dbConnect, debug *string, createdb *bool) error {
}
defer db.Close()

q, err := zdb.Load(db, "db.query."+query[0]+".sql")
q, _, err := zdb.Load(db, "db.query."+query[0]+".sql")
if err != nil {
q = strings.Join(query, " ")
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/goatcounter/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import (

"github.com/go-chi/chi/v5"
"github.com/teamwork/reload"
"golang.org/x/text/language"
"zgo.at/blackmail"
"zgo.at/errors"
"zgo.at/goatcounter/v2"
"zgo.at/goatcounter/v2/acme"
"zgo.at/goatcounter/v2/bgrun"
"zgo.at/goatcounter/v2/cron"
"zgo.at/goatcounter/v2/handlers"
"zgo.at/z18n"
"zgo.at/zdb"
"zgo.at/zhttp"
"zgo.at/zli"
Expand Down Expand Up @@ -363,6 +365,8 @@ func setupServe(dbConnect, dbConn string, dev bool, flagTLS string, automigrate
return nil, nil, nil, nil, 0, err
}

ctx = z18n.With(ctx, z18n.NewBundle(language.English).Locale("en"))

if dev && (!zio.Exists("db/migrate") || !zio.Exists("tpl") || !zio.Exists("public")) {
return nil, nil, nil, nil, 0, errors.New("-dev flag was given but this doesn't seem like a GoatCounter source directory")
}
Expand Down
5 changes: 3 additions & 2 deletions cron/language_stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"zgo.at/errors"
"zgo.at/goatcounter/v2"
"zgo.at/zdb"
"zgo.at/zstd/ztype"
)

func updateLanguageStats(ctx context.Context, hits []goatcounter.Hit) error {
Expand All @@ -29,11 +30,11 @@ func updateLanguageStats(ctx context.Context, hits []goatcounter.Hit) error {
}

day := h.CreatedAt.Format("2006-01-02")
k := day + h.Language + strconv.FormatInt(h.PathID, 10)
k := day + ztype.Deref(h.Language, "") + strconv.FormatInt(h.PathID, 10)
v := grouped[k]
if v.count == 0 {
v.day = day
v.language = h.Language
v.language = ztype.Deref(h.Language, "")
v.pathID = h.PathID
}

Expand Down
20 changes: 20 additions & 0 deletions db/query/hit_list.ListPathsLike.gotxt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
with x as (
select path_id, path, title from paths
where site_id = :site and (
{{if .match_case}}
path like :search
{{if .match_title}}or title like :search{{end}}
{{else}}
lower(path) like lower(:search)
{{if .match_title}}or lower(title) like lower(:search){{end}}
{{end}}
)
)
select
path_id, path, title,
sum(total) as count
from hit_counts
join x using(path_id)
where site_id = :site
group by path_id, path, title
order by count desc
13 changes: 0 additions & 13 deletions db/query/hit_list.ListPathsLike.sql

This file was deleted.

10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
golang.org/x/text v0.3.8
golang.org/x/text v0.4.0
zgo.at/blackmail v0.0.0-20211212060815-1f8e8a94692b
zgo.at/errors v1.1.0
zgo.at/follow v0.0.0-20211017230838-112008350298
Expand All @@ -30,23 +30,23 @@ require (
zgo.at/json v0.0.0-20211017213340-cc8bf51df08c
zgo.at/termtext v1.1.0
zgo.at/tz v0.0.0-20211017223207-006eae99adf6
zgo.at/z18n v0.0.0-20220606095325-513ddb98b28f
zgo.at/z18n v0.0.0-20221018165830-c235ed037573
zgo.at/zcache v1.2.0
zgo.at/zcache/v2 v2.1.0
zgo.at/zdb v0.0.0-20220822042559-7b1209555166
zgo.at/zdb v0.0.0-20221018164415-44f0726b1ff7
zgo.at/zhttp v0.0.0-20221001220656-a9e60fe5f8e3
zgo.at/zli v0.0.0-20221012220610-d6a5a841b943
zgo.at/zlog v0.0.0-20211008102840-46c1167bf2a9
zgo.at/zprof v0.0.0-20211217104121-c3c12596d8f0
zgo.at/zstd v0.0.0-20221013104704-16fa49fadc62
zgo.at/ztpl v0.0.0-20211128061406-6ff34b1256c4
zgo.at/ztpl v0.0.0-20221018165743-cbd2a654b6af
zgo.at/zvalidate v0.0.0-20211128195927-d13b18611e62
)

require (
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/lib/pq v1.10.6 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/oschwald/maxminddb-golang v1.8.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
Expand Down
23 changes: 10 additions & 13 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion handlers/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestBackendTpl(t *testing.T) {
{"/settings/users", "Access"},
{"/settings/users/add", "Password"},
{"/settings/users/1", "Password"},
{"/settings/purge", "Remove all pageviews for a page"},
{"/settings/purge", "Remove or merge pageviews"},
{"/settings/export", "includes all pageviews"},
{"/settings/delete-account", "The site will be marked as deleted"},
{"/settings/change-code", "Change your site code and login domain"},
Expand Down
3 changes: 2 additions & 1 deletion handlers/count.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ func (h backend) count(w http.ResponseWriter, r *http.Request) error {
if len(tags) > 0 {
base, c := tags[0].Base()
if c == language.Exact || c == language.High {
hit.Language = base.ISO3()
l := base.ISO3()
hit.Language = &l
}
}
}
Expand Down
74 changes: 51 additions & 23 deletions handlers/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,9 @@ func (h settings) mount(r chi.Router) {
set.Get("/settings/change-code", zhttp.Wrap(h.changeCode))
set.Post("/settings/change-code", zhttp.Wrap(h.changeCode))

set.Get("/settings/purge", zhttp.Wrap(func(w http.ResponseWriter, r *http.Request) error {
return h.purge(nil)(w, r)
}))
set.Get("/settings/purge/confirm", zhttp.Wrap(h.purgeConfirm))
set.Get("/settings/purge", zhttp.Wrap(h.purge))
set.Post("/settings/purge", zhttp.Wrap(h.purgeDo))
set.Post("/settings/merge", zhttp.Wrap(h.merge))

set.Get("/settings/export", zhttp.Wrap(func(w http.ResponseWriter, r *http.Request) error {
return h.export(nil)(w, r)
Expand Down Expand Up @@ -427,30 +425,35 @@ func (h settings) sitesCopySettings(w http.ResponseWriter, r *http.Request) erro
return zhttp.SeeOther(w, "/settings/sites")
}

func (h settings) purge(verr *zvalidate.Validator) zhttp.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
return zhttp.Template(w, "settings_purge.gohtml", struct {
Globals
Validate *zvalidate.Validator
}{newGlobals(w, r), verr})
}
}
func (h settings) purge(w http.ResponseWriter, r *http.Request) error {
var (
path = strings.TrimSpace(r.URL.Query().Get("path"))
matchTitle = r.URL.Query().Get("match-title") == "on"
matchCase = r.URL.Query().Get("match-case") == "on"
list goatcounter.HitLists
paths goatcounter.Paths
)

func (h settings) purgeConfirm(w http.ResponseWriter, r *http.Request) error {
path := strings.TrimSpace(r.URL.Query().Get("path"))
title := r.URL.Query().Get("match-title") == "on"
if path != "" {
err := list.ListPathsLike(r.Context(), path, matchTitle, matchCase)
if err != nil {
return err
}

var list goatcounter.HitLists
err := list.ListPathsLike(r.Context(), path, title)
if err != nil {
return err
err = paths.List(r.Context(), goatcounter.MustGetSite(r.Context()).ID)
if err != nil {
return err
}
}

return zhttp.Template(w, "settings_purge_confirm.gohtml", struct {
return zhttp.Template(w, "settings_purge.gohtml", struct {
Globals
PurgePath string
List goatcounter.HitLists
}{newGlobals(w, r), path, list})
PurgePath string
MatchTitle bool
MatchCase bool
List goatcounter.HitLists
AllPaths goatcounter.Paths
}{newGlobals(w, r), path, matchTitle, matchCase, list, paths})
}

func (h settings) purgeDo(w http.ResponseWriter, r *http.Request) error {
Expand All @@ -472,6 +475,31 @@ func (h settings) purgeDo(w http.ResponseWriter, r *http.Request) error {
return zhttp.SeeOther(w, "/settings/purge")
}

func (h settings) merge(w http.ResponseWriter, r *http.Request) error {
paths, err := zint.Split(r.Form.Get("paths"), ",")
if err != nil {
return err
}

v := goatcounter.NewValidate(r.Context())
dst := v.Integer("merge_with", r.Form.Get("merge_with"))
if v.HasErrors() {
return v
}

ctx := goatcounter.CopyContextValues(r.Context())
bgrun.Run(fmt.Sprintf("merge:%d", Site(ctx).ID), func() {
var list goatcounter.Hits
err := list.Merge(ctx, dst, paths)
if err != nil {
zlog.Error(err)
}
})

zhttp.Flash(w, T(r.Context(), "notify/started-background-process|Started in the background; may take about 10-20 seconds to fully process."))
return zhttp.SeeOther(w, "/settings/purge")
}

func (h settings) export(verr *zvalidate.Validator) zhttp.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
var exports goatcounter.Exports
Expand Down
2 changes: 1 addition & 1 deletion handlers/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestSettingsTpl(t *testing.T) {
}...)
},
router: newBackend,
path: "/settings/purge/confirm?path=/asd",
path: "/settings/purge?path=/asd",
auth: true,
wantCode: 200,
wantBody: "<tr><td>2</td><td>/asd</td><td>AAA</td></tr>",
Expand Down
36 changes: 35 additions & 1 deletion hit.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type Hit struct {
RefScheme *string `db:"ref_scheme" json:"-"`
UserAgentHeader string `db:"-" json:"-"`
Location string `db:"location" json:"-"`
Language string `db:"language" json:"-"`
Language *string `db:"language" json:"-"`
FirstVisit zbool.Bool `db:"first_visit" json:"-"`
CreatedAt time.Time `db:"created_at" json:"-"`

Expand All @@ -57,6 +57,9 @@ type Hit struct {
UserSessionID string `db:"-" json:"-"`
BrowserID int64 `db:"-" json:"-"`
SystemID int64 `db:"-" json:"-"`

// Don't process in memstore; for merging paths.
noProcess bool `db:"-" json:"-"`
}

func (h *Hit) Ignore() bool {
Expand Down Expand Up @@ -378,3 +381,34 @@ func (h *Hits) Purge(ctx context.Context, pathIDs []int64) error {
return nil
})
}

// Merge the given paths.
func (h *Hits) Merge(ctx context.Context, dst int64, pathIDs []int64) error {
site := MustGetSite(ctx).ID

err := (&Path{}).ByID(ctx, dst) // Ensure this site owns the path.
if err != nil {
return errors.Wrap(err, "Hits.Merge")
}

// Push back to lot to memstore to re-add it again, and then just call
// Purge() to delete the old ones.
err = zdb.Select(ctx, h, `select * from hits where site_id=? and path_id in (?)`, site, pathIDs)
if err != nil {
return errors.Wrap(err, "Hits.Merge")
}
hh := *h
for i := range hh {
hh[i].PathID = dst
hh[i].noProcess = true
}

err = errors.Wrap(h.Purge(ctx, pathIDs), "Hits.Merge")
if err != nil {
return errors.Wrap(err, "Hits.Merge")
}

// Only push back if delete worked.
Memstore.Append(hh...)
return nil
}
3 changes: 2 additions & 1 deletion hit_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ func (h *HitList) SiteTotalUTC(ctx context.Context, rng ztime.Range) error {
type HitLists []HitList

// ListPathsLike lists all paths matching the like pattern.
func (h *HitLists) ListPathsLike(ctx context.Context, search string, matchTitle bool) error {
func (h *HitLists) ListPathsLike(ctx context.Context, search string, matchTitle, matchCase bool) error {
err := zdb.Select(ctx, h, "load:hit_list.ListPathsLike", zdb.P{
"site": MustGetSite(ctx).ID,
"search": search,
"match_title": matchTitle,
"match_case": matchCase,
})
return errors.Wrap(err, "Hits.ListPathsLike")
}
Expand Down
Loading

0 comments on commit 02db82c

Please sign in to comment.