repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
9b70197
parent
1e40160
author
Eric Bower
date
2024-03-26 13:30:59 +0000 UTC
refactor(analytics): cleanup naming and fn params
6 files changed,  +94, -29
M cmd/scripts/analytics/analytics.go
+11, -6
 1@@ -3,22 +3,27 @@ package main
 2 import (
 3 	"log/slog"
 4 	"os"
 5-	"time"
 6 
 7+	"github.com/picosh/pico/db"
 8 	"github.com/picosh/pico/db/postgres"
 9+	"github.com/picosh/pico/shared"
10 )
11 
12 func main() {
13 	logger := slog.Default()
14 	DbURL := os.Getenv("DATABASE_URL")
15 	dbpool := postgres.NewDB(DbURL, logger)
16-	now := time.Now()
17+
18+	args := os.Args
19+	userID := args[1]
20 
21 	stats, err := dbpool.VisitSummary(
22-		"5dabf12d-f0d7-44f1-9557-32a043ffac37",
23-		"user_id",
24-		"day",
25-		now.AddDate(0, 0, -now.Day()+1),
26+		&db.SummarOpts{
27+			FkID:     userID,
28+			By:       "user_id",
29+			Interval: "day",
30+			Origin:   shared.StartOfMonth(),
31+		},
32 	)
33 	if err != nil {
34 		panic(err)
M db/db.go
+19, -12
 1@@ -145,6 +145,13 @@ type Analytics struct {
 2 	UsersWithPost  int
 3 }
 4 
 5+type SummarOpts struct {
 6+	FkID     string
 7+	By       string
 8+	Interval string
 9+	Origin   time.Time
10+}
11+
12 type PostAnalytics struct {
13 	ID       string
14 	PostID   string
15@@ -166,23 +173,23 @@ type AnalyticsVisits struct {
16 }
17 
18 type VisitInterval struct {
19-	PostID    string
20-	ProjectID string
21-	Interval  *time.Time
22-	Visitors  int
23+	PostID    string     `json:"post_id"`
24+	ProjectID string     `json:"project_id"`
25+	Interval  *time.Time `json:"interval"`
26+	Visitors  int        `json:"visitors"`
27 }
28 
29 type VisitUrl struct {
30-	PostID    string
31-	ProjectID string
32-	Url       string
33-	Count     int
34+	PostID    string `json:"post_id"`
35+	ProjectID string `json:"project_id"`
36+	Url       string `json:"url"`
37+	Count     int    `json:"count"`
38 }
39 
40 type SummaryVisits struct {
41-	Intervals   []*VisitInterval
42-	TopUrls     []*VisitUrl
43-	TopReferers []*VisitUrl
44+	Intervals   []*VisitInterval `json:"intervals"`
45+	TopUrls     []*VisitUrl      `json:"top_urls"`
46+	TopReferers []*VisitUrl      `json:"top_referers"`
47 }
48 
49 type Pager struct {
50@@ -361,7 +368,7 @@ type DB interface {
51 	ReplaceAliasesForPost(aliases []string, postID string) error
52 
53 	InsertVisit(view *AnalyticsVisits) error
54-	VisitSummary(fkID, by, interval string, origin time.Time) (*SummaryVisits, error)
55+	VisitSummary(opts *SummarOpts) (*SummaryVisits, error)
56 
57 	AddPicoPlusUser(username string, paymentType, txId string) error
58 	FindFeatureForUser(userID string, feature string) (*FeatureFlag, error)
M db/postgres/storage.go
+4, -4
 1@@ -1146,16 +1146,16 @@ func (me *PsqlDB) visitUrl(fkID, by string, origin time.Time) ([]*db.VisitUrl, e
 2 	return intervals, nil
 3 }
 4 
 5-func (me *PsqlDB) VisitSummary(fkID, by, interval string, origin time.Time) (*db.SummaryVisits, error) {
 6-	visitors, err := me.visitUnique(fkID, by, interval, origin)
 7+func (me *PsqlDB) VisitSummary(opts *db.SummarOpts) (*db.SummaryVisits, error) {
 8+	visitors, err := me.visitUnique(opts.FkID, opts.By, opts.Interval, opts.Origin)
 9 	if err != nil {
10 		return nil, err
11 	}
12-	urls, err := me.visitUrl(fkID, by, origin)
13+	urls, err := me.visitUrl(opts.FkID, opts.By, opts.Origin)
14 	if err != nil {
15 		return nil, err
16 	}
17-	refs, err := me.visitReferer(fkID, by, origin)
18+	refs, err := me.visitReferer(opts.FkID, opts.By, opts.Origin)
19 	if err != nil {
20 		return nil, err
21 	}
M shared/analytics.go
+16, -7
 1@@ -38,21 +38,25 @@ func trackableRequest(r *http.Request) error {
 2 func cleanIpAddress(ip string) (string, error) {
 3 	host, _, err := net.SplitHostPort(ip)
 4 	if err != nil {
 5-		return "", err
 6+		host = ip
 7 	}
 8-	// /16 IPv4 subnet mask
 9+	// /24 IPv4 subnet mask
10 	// /64 IPv6 subnet mask
11 	anonymizer := ipanonymizer.NewWithMask(
12-		net.CIDRMask(16, 32),
13+		net.CIDRMask(24, 32),
14 		net.CIDRMask(64, 128),
15 	)
16 	anonIp, err := anonymizer.IPString(host)
17 	return anonIp, err
18 }
19 
20-func cleanUrl(curl *url.URL) (string, string) {
21+func cleanUrl(r *http.Request) (string, string) {
22+	host := r.Header.Get("x-forwarded-host")
23+	if host == "" {
24+		host = r.URL.Host
25+	}
26 	// we don't want query params in the url for security reasons
27-	return curl.Host, curl.Path
28+	return host, r.URL.Path
29 }
30 
31 func cleanUserAgent(ua string) string {
32@@ -86,11 +90,16 @@ func AnalyticsVisitFromRequest(r *http.Request, userID string, secret string) (*
33 		return nil, err
34 	}
35 
36-	ipAddress, err := cleanIpAddress(r.RemoteAddr)
37+	// https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#defaults
38+	ipOrig := r.Header.Get("x-forwarded-for")
39+	if ipOrig == "" {
40+		ipOrig = r.RemoteAddr
41+	}
42+	ipAddress, err := cleanIpAddress(ipOrig)
43 	if err != nil {
44 		return nil, err
45 	}
46-	host, path := cleanUrl(r.URL)
47+	host, path := cleanUrl(r)
48 
49 	referer, err := cleanReferer(r.Referer())
50 	if err != nil {
M shared/util.go
+10, -0
 1@@ -152,3 +152,13 @@ func Shasum(data []byte) string {
 2 func BytesToGB(size int) float32 {
 3 	return (((float32(size) / 1024) / 1024) / 1024)
 4 }
 5+
 6+func StartOfMonth() time.Time {
 7+	now := time.Now()
 8+	return now.AddDate(0, 0, -now.Day()+1)
 9+}
10+
11+func StartOfYear() time.Time {
12+	now := time.Now()
13+	return now.AddDate(-1, 0, 0)
14+}
M ui/api.go
+34, -0
 1@@ -521,6 +521,38 @@ func getProjectObjects(apiConfig *shared.ApiConfig, ctx ssh.Context) http.Handle
 2 	}
 3 }
 4 
 5+func getAnalytics(apiConfig *shared.ApiConfig, ctx ssh.Context, sumtype string) http.HandlerFunc {
 6+	logger := apiConfig.Cfg.Logger
 7+	dbpool := apiConfig.Dbpool
 8+	return func(w http.ResponseWriter, r *http.Request) {
 9+		w.Header().Set("Content-Type", "application/json")
10+		user, _ := shared.GetUserCtx(ctx)
11+		if !ensureUser(w, user) {
12+			return
13+		}
14+
15+		year := &db.SummarOpts{FkID: user.ID, By: "user_id", Interval: "month", Origin: shared.StartOfYear()}
16+		month := &db.SummarOpts{FkID: user.ID, By: "user_id", Interval: "day", Origin: shared.StartOfMonth()}
17+
18+		opts := year
19+		if sumtype == "month" {
20+			opts = month
21+		}
22+
23+		summary, err := dbpool.VisitSummary(opts)
24+		if err != nil {
25+			logger.Info("cannot fetch analytics", "err", err.Error())
26+			shared.JSONError(w, err.Error(), http.StatusUnprocessableEntity)
27+			return
28+		}
29+
30+		err = json.NewEncoder(w).Encode(&summary)
31+		if err != nil {
32+			logger.Error(err.Error())
33+		}
34+	}
35+}
36+
37 func CreateRoutes(apiConfig *shared.ApiConfig, ctx ssh.Context) []shared.Route {
38 	logger := apiConfig.Cfg.Logger
39 	pubkey, err := shared.GetPublicKeyCtx(ctx)
40@@ -551,5 +583,7 @@ func CreateRoutes(apiConfig *shared.ApiConfig, ctx ssh.Context) []shared.Route {
41 		shared.NewCorsRoute("GET", "/api/posts/prose", getPosts(apiConfig, ctx, "prose")),
42 		shared.NewCorsRoute("GET", "/api/posts/pastes", getPosts(apiConfig, ctx, "pastes")),
43 		shared.NewCorsRoute("GET", "/api/posts/feeds", getPosts(apiConfig, ctx, "feeds")),
44+		shared.NewCorsRoute("GET", "/api/analytics/year", getAnalytics(apiConfig, ctx, "year")),
45+		shared.NewCorsRoute("GET", "/api/analytics", getAnalytics(apiConfig, ctx, "month")),
46 	}
47 }