dashboard / erock/pico / feat(auth): subscribe to pico's metric-drain pipe #32 rss

accepted · opened on 2024-11-12T21:13:35Z by erock
Help
checkout latest patchset:
ssh pr.pico.sh print pr-32 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 32
add review to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add --review 32
accept PR:
ssh pr.pico.sh pr accept 32
close PR:
ssh pr.pico.sh pr close 32

Logs

erock created pr with ps-66 on 2024-11-12T21:13:35Z
erock added ps-73 on 2024-11-14T15:42:32Z
erock changed status on 2024-11-23T03:34:16Z {"status":"accepted"}

Patchsets

ps-66 by erock on 2024-11-12T21:13:35Z
Range Diff ↕ rd-73
1: 7ec3569 = 1: 7ec3569 feat(auth): subscribe to pico's metric-drain pipe
2: 8a197f0 = 2: 8a197f0 chore: update pubsub
3: d4bda15 = 3: d4bda15 refactor: use pipe for analytics
-: ------- > 4: 2b7c358 chore: prep for release
ps-73 by erock on 2024-11-14T15:42:32Z

Range-diff rd-73

title
feat(auth): subscribe to pico's metric-drain pipe
description
Patch equal
old #1
7ec3569
new #1
7ec3569
title
chore: update pubsub
description
Patch equal
old #2
8a197f0
new #2
8a197f0
title
refactor: use pipe for analytics
description
Patch equal
old #3
d4bda15
new #3
d4bda15
title
chore: prep for release
description
Patch added
old #0
(none)
new #4
2b7c358
Back to top
1: 7ec3569 = 1: 7ec3569 feat(auth): subscribe to pico's metric-drain pipe
2: 8a197f0 = 2: 8a197f0 chore: update pubsub
3: d4bda15 = 3: d4bda15 refactor: use pipe for analytics
-: ------- > 4: 2b7c358 chore: prep for release

old


                    

new

old:Makefile new:Makefile
 	$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20240324_add_analytics_table.sql
 	$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20240819_add_projects_blocked.sql
 	$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241028_add_analytics_indexes.sql
+	$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241114_add_namespace_to_analytics.sql
 .PHONY: migrate
 
 latest:
-	$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241028_add_analytics_indexes.sql
+	$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241114_add_namespace_to_analytics.sql
 .PHONY: latest
 
 psql:

old


                    

new

old:auth/api.go new:auth/api.go
 	"context"
 	"crypto/hmac"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"html/template"
 	"io"
 	}
 }
 
-func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger) {
+func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secret string) {
 	conn := shared.NewPicoPipeClient()
 	stdoutPipe, err := pubsub.RemoteSub("sub metric-drain -k", ctx, conn)
 
 			logger.Error("json unmarshal", "err", err)
 			continue
 		}
+
+		err = shared.AnalyticsVisitFromVisit(&view, dbpool, secret)
+		if err != nil {
+			if !errors.Is(err, shared.ErrAnalyticsDisabled) {
+				logger.Info("could not record analytics view", "reason", err)
+			}
+		}
+
 		err = dbpool.InsertVisit(&view)
 		if err != nil {
 			logger.Error("could not insert view record", "err", err)
 	DbURL  string
 	Domain string
 	Issuer string
+	Secret string
 }
 
 func StartApiServer() {
 		Issuer: utils.GetEnv("AUTH_ISSUER", "pico.sh"),
 		Domain: utils.GetEnv("AUTH_DOMAIN", "http://0.0.0.0:3000"),
 		Port:   utils.GetEnv("AUTH_WEB_PORT", "3000"),
+		Secret: utils.GetEnv("PICO_SECRET", ""),
+	}
+	if cfg.Secret == "" {
+		panic("must provide PICO_SECRET environment variable")
 	}
 
 	logger := shared.CreateLogger("auth")
 
 	ctx := context.Background()
 	// gather metrics in the auth service
-	go metricDrainSub(ctx, db, logger)
+	go metricDrainSub(ctx, db, logger, cfg.Secret)
 	defer ctx.Done()
 
 	routes := createMainRoutes()

old


                    

new

old:db/db.go new:db/db.go
 	UserID    string `json:"user_id"`
 	ProjectID string `json:"project_id"`
 	PostID    string `json:"post_id"`
+	Namespace string `json:"namespace"`
 	Host      string `json:"host"`
 	Path      string `json:"path"`
 	IpAddress string `json:"ip_adress"`

old


                    

new

old:db/postgres/storage.go new:db/postgres/storage.go
 	}
 }
 
-func (me *PsqlDB) InsertVisit(view *db.AnalyticsVisits) error {
+func (me *PsqlDB) InsertVisit(visit *db.AnalyticsVisits) error {
 	_, err := me.Db.Exec(
-		`INSERT INTO analytics_visits (user_id, project_id, post_id, host, path, ip_address, user_agent, referer, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);`,
-		view.UserID,
-		newNullString(view.ProjectID),
-		newNullString(view.PostID),
-		view.Host,
-		view.Path,
-		view.IpAddress,
-		view.UserAgent,
-		view.Referer,
-		view.Status,
+		`INSERT INTO analytics_visits (user_id, project_id, post_id, namespace, host, path, ip_address, user_agent, referer, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);`,
+		visit.UserID,
+		newNullString(visit.ProjectID),
+		newNullString(visit.PostID),
+		newNullString(visit.Namespace),
+		visit.Host,
+		visit.Path,
+		visit.IpAddress,
+		visit.UserAgent,
+		visit.Referer,
+		visit.Status,
 	)
 	return err
 }

old


                    

new

old:go.mod new:go.mod
 	github.com/muesli/termenv v0.15.3-0.20240912151726-82936c5ea257
 	github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577
 	github.com/picosh/pobj v0.0.0-20241016194248-c39198b2ff23
-	github.com/picosh/pubsub v0.0.0-20241112151357-866d44c53659
+	github.com/picosh/pubsub v0.0.0-20241114025640-35db438302b4
 	github.com/picosh/send v0.0.0-20241107150437-0febb0049b4f
 	github.com/picosh/tunkit v0.0.0-20240905223921-532404cef9d9
 	github.com/picosh/utils v0.0.0-20241018143404-b351d5d765f3

old


                    

new

old:go.sum new:go.sum
 github.com/picosh/go-rsync-receiver v0.0.0-20240709135253-1daf4b12a9fc/go.mod h1:i0iR3W4GSm1PuvVxB9OH32E5jP+CYkVb2NQSe0JCtlo=
 github.com/picosh/pobj v0.0.0-20241016194248-c39198b2ff23 h1:NEJ5a4UXeF0/X7xmYNzXcwLQID9DwgazlqkMMC5zZ3M=
 github.com/picosh/pobj v0.0.0-20241016194248-c39198b2ff23/go.mod h1:cF+eAl4G1vU+WOD8cYCKaxokHo6MWmbR8J4/SJnvESg=
-github.com/picosh/pubsub v0.0.0-20241112151357-866d44c53659 h1:HmRi+QkAcKkOcLD90xbf7qZy95muQEd/DqttK9xtpHk=
-github.com/picosh/pubsub v0.0.0-20241112151357-866d44c53659/go.mod h1:m6ZZpg+lZB3XTIKlbSqQgi4NrBPtARv23b8vGYDoCo4=
+github.com/picosh/pubsub v0.0.0-20241114025640-35db438302b4 h1:pITSRXb9NDGdC6AmuS3JE+8Ek4/pUG7tXJPP3cOaqf4=
+github.com/picosh/pubsub v0.0.0-20241114025640-35db438302b4/go.mod h1:m6ZZpg+lZB3XTIKlbSqQgi4NrBPtARv23b8vGYDoCo4=
 github.com/picosh/send v0.0.0-20241107150437-0febb0049b4f h1:pdEh1Z7zH5Og9nS7jRuqwup3bcPsC6faDNQ6mgrV9ws=
 github.com/picosh/send v0.0.0-20241107150437-0febb0049b4f/go.mod h1:RAgLDK3LrDK6pNeXtU9tjo28obl5DxShcTUk2nm/KCM=
 github.com/picosh/senpai v0.0.0-20240503200611-af89e73973b0 h1:pBRIbiCj7K6rGELijb//dYhyCo8A3fvxW5dijrJVtjs=

old


                    

new

old:pgs/config.go new:pgs/config.go
 	minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
 	minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
 	dbURL := utils.GetEnv("DATABASE_URL", "")
-	secret := utils.GetEnv("PICO_SECRET", "")
-	if secret == "" {
-		panic("must provide PICO_SECRET environment variable")
-	}
 
 	cfg := shared.ConfigSite{
-		Secret:             secret,
 		Domain:             domain,
 		Port:               port,
 		Protocol:           protocol,

old


                    

new

old:pgs/web_asset_handler.go new:pgs/web_asset_handler.go
 		)
 		// track 404s
 		ch := h.AnalyticsQueue
-		view, err := shared.AnalyticsVisitFromRequest(r, h.Dbpool, h.UserID, h.Cfg.Secret)
+		view, err := shared.AnalyticsVisitFromRequest(r, h.Dbpool, h.UserID)
 		if err == nil {
 			view.ProjectID = h.ProjectID
 			view.Status = http.StatusNotFound
 	if finContentType == "text/html" {
 		// track visit
 		ch := h.AnalyticsQueue
-		view, err := shared.AnalyticsVisitFromRequest(r, h.Dbpool, h.UserID, h.Cfg.Secret)
+		view, err := shared.AnalyticsVisitFromRequest(r, h.Dbpool, h.UserID)
 		if err == nil {
 			view.ProjectID = h.ProjectID
 			ch <- view

old


                    

new

old:prose/api.go new:prose/api.go
 
 	// track visit
 	ch := shared.GetAnalyticsQueue(r)
-	view, err := shared.AnalyticsVisitFromRequest(r, dbpool, user.ID, cfg.Secret)
+	view, err := shared.AnalyticsVisitFromRequest(r, dbpool, user.ID)
 	if err == nil {
 		ch <- view
 	} else {
 		}
 
 		// track visit
-		view, err := shared.AnalyticsVisitFromRequest(r, dbpool, user.ID, cfg.Secret)
+		view, err := shared.AnalyticsVisitFromRequest(r, dbpool, user.ID)
 		if err == nil {
 			view.PostID = post.ID
 			ch <- view

old


                    

new

old:prose/config.go new:prose/config.go
 	dbURL := utils.GetEnv("DATABASE_URL", "")
 	maxSize := uint64(500 * utils.MB)
 	maxImgSize := int64(10 * utils.MB)
-	secret := utils.GetEnv("PICO_SECRET", "")
-	if secret == "" {
-		panic("must provide PICO_SECRET environment variable")
-	}
 
 	return &shared.ConfigSite{
 		Debug:      debug == "1",
-		Secret:     secret,
 		Domain:     domain,
 		Port:       port,
 		Protocol:   protocol,

old


                    

new

old:shared/analytics.go new:shared/analytics.go
 	return hex.EncodeToString(dataHmac)
 }
 
-func trackableRequest(r *http.Request) error {
-	agent := r.UserAgent()
+func trackableUserAgent(agent string) error {
 	// dont store requests from bots
 	if crawlerdetect.IsCrawler(agent) {
 		return fmt.Errorf(
 	return nil
 }
 
+func trackableRequest(r *http.Request) error {
+	agent := r.UserAgent()
+	return trackableUserAgent(agent)
+}
+
 func cleanIpAddress(ip string) (string, error) {
 	host, _, err := net.SplitHostPort(ip)
 	if err != nil {
 	return anonIp, err
 }
 
-func cleanUrl(r *http.Request) (string, string) {
+func cleanUrl(orig string) (string, string) {
+	u, err := url.Parse(orig)
+	if err != nil {
+		return "", ""
+	}
+	return u.Host, u.Path
+}
+
+func cleanUrlFromRequest(r *http.Request) (string, string) {
 	host := r.Header.Get("x-forwarded-host")
 	if host == "" {
 		host = r.URL.Host
 
 var ErrAnalyticsDisabled = errors.New("owner does not have site analytics enabled")
 
-func AnalyticsVisitFromRequest(r *http.Request, dbpool db.DB, userID string, secret string) (*db.AnalyticsVisits, error) {
-	if !dbpool.HasFeatureForUser(userID, "analytics") {
-		return nil, ErrAnalyticsDisabled
+func AnalyticsVisitFromVisit(visit *db.AnalyticsVisits, dbpool db.DB, secret string) error {
+	if !dbpool.HasFeatureForUser(visit.UserID, "analytics") {
+		return ErrAnalyticsDisabled
 	}
 
-	err := trackableRequest(r)
+	err := trackableUserAgent(visit.UserAgent)
 	if err != nil {
-		return nil, err
+		return err
+	}
+
+	ipAddress, err := cleanIpAddress(visit.IpAddress)
+	if err != nil {
+		return err
+	}
+	visit.IpAddress = HmacString(secret, ipAddress)
+	_, path := cleanUrl(visit.Path)
+	visit.Path = path
+
+	referer, err := cleanReferer(visit.Referer)
+	if err != nil {
+		return err
 	}
+	visit.Referer = referer
+	visit.UserAgent = cleanUserAgent(visit.UserAgent)
 
+	return nil
+}
+
+func ipFromRequest(r *http.Request) string {
 	// https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#defaults
 	ipOrig := r.Header.Get("x-forwarded-for")
 	if ipOrig == "" {
 			ipOrig = sshCtx.RemoteAddr().String()
 		}
 	}
-	ipAddress, err := cleanIpAddress(ipOrig)
-	if err != nil {
-		return nil, err
+
+	return ipOrig
+}
+
+func AnalyticsVisitFromRequest(r *http.Request, dbpool db.DB, userID string) (*db.AnalyticsVisits, error) {
+	if !dbpool.HasFeatureForUser(userID, "analytics") {
+		return nil, ErrAnalyticsDisabled
 	}
-	host, path := cleanUrl(r)
 
-	referer, err := cleanReferer(r.Referer())
+	err := trackableRequest(r)
 	if err != nil {
 		return nil, err
 	}
 
+	ipAddress := ipFromRequest(r)
+	host, path := cleanUrlFromRequest(r)
+
 	return &db.AnalyticsVisits{
 		UserID:    userID,
 		Host:      host,
 		Path:      path,
-		IpAddress: HmacString(secret, ipAddress),
-		UserAgent: cleanUserAgent(r.UserAgent()),
-		Referer:   referer,
+		IpAddress: ipAddress,
+		UserAgent: r.UserAgent(),
+		Referer:   r.Referer(),
 		Status:    http.StatusOK,
 	}, nil
 }

old


                    

new

old:shared/config.go new:shared/config.go
 type ConfigSite struct {
 	Debug              bool
 	SendgridKey        string
-	Secret             string
 	Domain             string
 	Port               string
 	PortOverride       string

old


                    

new

new:sql/migrations/20241114_add_namespace_to_analytics.sql
+ALTER TABLE analytics_visits ADD COLUMN namespace varchar(256);