- 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
+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)
+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 }
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 {
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+}
+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 }