pico
created pr with
35.1
added 35.2
1: 77aaa29 ! 1: 8d56535 reactor(metric-drain): use caddy json format
-: ------- > 2: a336041 wip
-: ------- > 3: 7ae45b3 chore: wrap
-: ------- > 4: bfa5c4f done
added 35.3
1: 8d56535 < -: ------- reactor(metric-drain): use caddy json format
-: ------- > 1: c7eeb12 reactor(metric-drain): use caddy access logs
2: a336041 < -: ------- wip
3: 7ae45b3 < -: ------- chore: wrap
4: bfa5c4f < -: ------- done
added 35.4
1: c7eeb12 ! 1: 4e0839a reactor(metric-drain): use caddy access logs
changed status to
accepted
cmds
checkout latest patchset:
ssh pr.pico.sh print 35 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 35.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 35set PR to open (enables RSS notifications):
ssh pr.pico.sh pr open 35set PR to draft (stops RSS notifications):
ssh pr.pico.sh pr draft 35
Patchset
35.3
reactor(metric-drain): use caddy access logs
Eric Bower
2024-11-15T15:02:24ZPreviously we were sending site usage analytics within our web app code. This worked well for our use case because we could filter, parse, and send the analytics to our pipe `metric-drain` which would then store the analytics into our database. Because we want to enable HTTP caching for pgs we won't always reach our web app code since usage analytics will terminate at our cache layer. Instead, we want to record analytics higher in the request stack. In this case, we want to record site analytics from Caddy access logs. Here's how it works: - `pub` caddy access logs to our pipe `container-drain` - `auth/web` will `sub` to `container-drain`, filter, deserialize, and `pub` to `metric-drain` - `auth/web` will `sub` to `metric-drain` and store the analytics in our database
Semantic diff summary
8 added,
27 modified,
1 signature changed,
7 removed
across 13 analyzed files
(4 files skipped: unsupported file type)
auth/api.go
-
chunklines 14-20modified -
chunklines 22-28modified -
type_declarationAccessLogReqadded -
type_declarationRespHeadersadded -
type_declarationCaddyAccessLogadded -
function_declarationdeserializeCaddyAccessLogadded -
function_declarationcontainerDrainSubadded -
function_declarationaccessLogToVisitadded -
function_declarationmetricDrainSubmodified -
function_declarationStartApiServermodified
pgs/web.go
-
function_declarationStartApiServermodified -
function_declarationNewWebRoutersignature changed -
type_declarationWebRoutermodified -
function_declarationcheckHandlermodified -
function_declarationServeAssetmodified -
type_declarationSubdomainPropsremoved -
function_declarationgetProjectFromSubdomainremoved
+2
-1
Makefile
#
| ... | ... | @@ -135,10 +135,11 @@ migrate: | |
| 135 | 135 | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20240819_add_projects_blocked.sql | |
| 136 | 136 | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241028_add_analytics_indexes.sql | |
| 137 | 137 | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241114_add_namespace_to_analytics.sql | |
| 138 | + | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241125_add_content_type_to_analytics.sql | |
| 138 | 139 | .PHONY: migrate | |
| 139 | 140 | ||
| 140 | 141 | latest: | |
| 141 | - | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241114_add_namespace_to_analytics.sql | |
| 142 | + | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241125_add_content_type_to_analytics.sql | |
| 142 | 143 | .PHONY: latest | |
| 143 | 144 | ||
| 144 | 145 | psql: |
+162
-12
auth/api.go
#
| ... | ... | @@ -578,6 +580,155 @@ func checkoutHandler() http.HandlerFunc { | |
| 578 | 580 | } | |
| 579 | 581 | } | |
| 580 | 582 | ||
| 583 | + | type AccessLogReq struct { | |
| 584 | + | RemoteIP string `json:"remote_ip"` | |
| 585 | + | RemotePort string `json:"remote_port"` | |
| 586 | + | ClientIP string `json:"client_ip"` | |
| 587 | + | Method string `json:"method"` | |
| 588 | + | Host string `json:"host"` | |
| 589 | + | Uri string `json:"uri"` | |
| 590 | + | Headers struct { | |
| 591 | + | UserAgent []string `json:"User-Agent"` | |
| 592 | + | Referer []string `json:"Referer"` | |
| 593 | + | } `json:"headers"` | |
| 594 | + | Tls struct { | |
| 595 | + | ServerName string `json:"server_name"` | |
| 596 | + | } `json:"tls"` | |
| 597 | + | } | |
| 598 | + | ||
| 599 | + | type RespHeaders struct { | |
| 600 | + | ContentType []string `json:"Content-Type"` | |
| 601 | + | } | |
| 602 | + | ||
| 603 | + | type CaddyAccessLog struct { | |
| 604 | + | Request AccessLogReq `json:"request"` | |
| 605 | + | Status int `json:"status"` | |
| 606 | + | RespHeaders RespHeaders `json:"resp_headers"` | |
| 607 | + | } | |
| 608 | + | ||
| 609 | + | func deserializeCaddyAccessLog(dbpool db.DB, access *CaddyAccessLog) (*db.AnalyticsVisits, error) { | |
| 610 | + | spaceRaw := strings.SplitN(access.Request.Tls.ServerName, ".", 2) | |
| 611 | + | space := spaceRaw[0] | |
| 612 | + | host := access.Request.Host | |
| 613 | + | path := access.Request.Uri | |
| 614 | + | subdomain := "" | |
| 615 | + | ||
| 616 | + | // grab subdomain based on host | |
| 617 | + | if strings.HasSuffix(host, "tuns.sh") { | |
| 618 | + | subdomain = strings.TrimSuffix(host, ".tuns.sh") | |
| 619 | + | } else if strings.HasSuffix(host, "pgs.sh") { | |
| 620 | + | subdomain = strings.TrimSuffix(host, ".pgs.sh") | |
| 621 | + | } else if strings.HasSuffix(host, "prose.sh") { | |
| 622 | + | subdomain = strings.TrimSuffix(host, ".prose.sh") | |
| 623 | + | } else { | |
| 624 | + | subdomain = shared.GetCustomDomain(host, space) | |
| 625 | + | } | |
| 626 | + | ||
| 627 | + | // get user and namespace details from subdomain | |
| 628 | + | props, err := shared.GetProjectFromSubdomain(subdomain) | |
| 629 | + | if err != nil { | |
| 630 | + | return nil, err | |
| 631 | + | } | |
| 632 | + | // get user ID | |
| 633 | + | user, err := dbpool.FindUserForName(props.Username) | |
| 634 | + | if err != nil { | |
| 635 | + | return nil, err | |
| 636 | + | } | |
| 637 | + | ||
| 638 | + | projectID := "" | |
| 639 | + | postID := "" | |
| 640 | + | if space == "pgs" { // figure out project ID | |
| 641 | + | project, err := dbpool.FindProjectByName(user.ID, props.ProjectName) | |
| 642 | + | if err != nil { | |
| 643 | + | return nil, err | |
| 644 | + | } | |
| 645 | + | projectID = project.ID | |
| 646 | + | } else if space == "prose" { // figure out post ID | |
| 647 | + | if path == "" || path == "/" { | |
| 648 | + | } else { | |
| 649 | + | post, err := dbpool.FindPostWithSlug(path, user.ID, space) | |
| 650 | + | if err != nil { | |
| 651 | + | return nil, err | |
| 652 | + | } | |
| 653 | + | postID = post.ID | |
| 654 | + | } | |
| 655 | + | } | |
| 656 | + | ||
| 657 | + | return &db.AnalyticsVisits{ | |
| 658 | + | UserID: user.ID, | |
| 659 | + | ProjectID: projectID, | |
| 660 | + | PostID: postID, | |
| 661 | + | Namespace: space, | |
| 662 | + | Host: host, | |
| 663 | + | Path: path, | |
| 664 | + | IpAddress: access.Request.ClientIP, | |
| 665 | + | UserAgent: strings.Join(access.Request.Headers.UserAgent, " "), | |
| 666 | + | Referer: strings.Join(access.Request.Headers.Referer, " "), | |
| 667 | + | ContentType: strings.Join(access.RespHeaders.ContentType, " "), | |
| 668 | + | Status: access.Status, | |
| 669 | + | }, nil | |
| 670 | + | } | |
| 671 | + | ||
| 672 | + | // this feels really stupid because i'm taking containter-drain, | |
| 673 | + | // filtering it, and then sending it to metric-drain. The | |
| 674 | + | // metricDrainSub function listens on the metric-drain and saves it. | |
| 675 | + | // So why not just call the necessary functions to save the visit? | |
| 676 | + | // We want to be able to use pipe as a debugging tool which means we | |
| 677 | + | // can manually sub to `metric-drain` and have a nice clean output to view. | |
| 678 | + | func containerDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger) { | |
| 679 | + | info := shared.NewPicoPipeClient() | |
| 680 | + | drain := pipe.NewReconnectReadWriteCloser( | |
| 681 | + | ctx, | |
| 682 | + | logger, | |
| 683 | + | info, | |
| 684 | + | "container drain", | |
| 685 | + | "sub container-drain -k", | |
| 686 | + | 100, | |
| 687 | + | -1, | |
| 688 | + | ) | |
| 689 | + | ||
| 690 | + | send := pipe.NewReconnectReadWriteCloser( | |
| 691 | + | ctx, | |
| 692 | + | logger, | |
| 693 | + | info, | |
| 694 | + | "from container drain to metric drain", | |
| 695 | + | "pub metric-drain -b=false", | |
| 696 | + | 100, | |
| 697 | + | -1, | |
| 698 | + | ) | |
| 699 | + | ||
| 700 | + | for { | |
| 701 | + | scanner := bufio.NewScanner(drain) | |
| 702 | + | for scanner.Scan() { | |
| 703 | + | line := scanner.Text() | |
| 704 | + | if strings.Contains(line, "http.log.access") { | |
| 705 | + | clean := strings.TrimSpace(line) | |
| 706 | + | visit, err := accessLogToVisit(dbpool, clean) | |
| 707 | + | if err != nil { | |
| 708 | + | logger.Debug("could not convert access log to a visit", "err", err) | |
| 709 | + | continue | |
| 710 | + | } | |
| 711 | + | jso, err := json.Marshal(visit) | |
| 712 | + | if err != nil { | |
| 713 | + | logger.Error("could not marshal json of a visit", "err", err) | |
| 714 | + | continue | |
| 715 | + | } | |
| 716 | + | _, _ = send.Write(jso) | |
| 717 | + | } | |
| 718 | + | } | |
| 719 | + | } | |
| 720 | + | } | |
| 721 | + | ||
| 722 | + | func accessLogToVisit(dbpool db.DB, line string) (*db.AnalyticsVisits, error) { | |
| 723 | + | accessLog := CaddyAccessLog{} | |
| 724 | + | err := json.Unmarshal([]byte(line), &accessLog) | |
| 725 | + | if err != nil { | |
| 726 | + | return nil, err | |
| 727 | + | } | |
| 728 | + | ||
| 729 | + | return deserializeCaddyAccessLog(dbpool, &accessLog) | |
| 730 | + | } | |
| 731 | + | ||
| 581 | 732 | func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secret string) { | |
| 582 | 733 | drain := metrics.ReconnectReadMetrics( | |
| 583 | 734 | ctx, |
| ... | ... | @@ -594,30 +745,26 @@ func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secr | |
| 594 | 745 | visit := db.AnalyticsVisits{} | |
| 595 | 746 | err := json.Unmarshal([]byte(line), &visit) | |
| 596 | 747 | if err != nil { | |
| 597 | - | logger.Error("json unmarshal", "err", err) | |
| 748 | + | logger.Info("could not unmarshal json", "err", err, "line", line) | |
| 598 | 749 | continue | |
| 599 | 750 | } | |
| 600 | - | ||
| 601 | - | user := slog.Any("userId", visit.UserID) | |
| 602 | - | ||
| 603 | 751 | err = shared.AnalyticsVisitFromVisit(&visit, dbpool, secret) | |
| 604 | 752 | if err != nil { | |
| 605 | 753 | if !errors.Is(err, shared.ErrAnalyticsDisabled) { | |
| 606 | - | logger.Info("could not record analytics visit", "reason", err, "visit", visit, user) | |
| 607 | - | continue | |
| 754 | + | logger.Info("could not record analytics visit", "reason", err) | |
| 608 | 755 | } | |
| 609 | 756 | } | |
| 610 | 757 | ||
| 611 | - | logger.Info("inserting visit", "visit", visit, user) | |
| 758 | + | if visit.ContentType != "" && !strings.HasPrefix(visit.ContentType, "text/html") { | |
| 759 | + | continue | |
| 760 | + | } | |
| 761 | + | ||
| 762 | + | logger.Info("inserting visit", "visit", visit) | |
| 612 | 763 | err = dbpool.InsertVisit(&visit) | |
| 613 | 764 | if err != nil { | |
| 614 | - | logger.Error("could not insert visit record", "err", err, "visit", visit, user) | |
| 765 | + | logger.Error("could not insert visit record", "err", err) | |
| 615 | 766 | } | |
| 616 | 767 | } | |
| 617 | - | ||
| 618 | - | if scanner.Err() != nil { | |
| 619 | - | logger.Error("scanner error", "err", scanner.Err()) | |
| 620 | - | } | |
| 621 | 768 | } | |
| 622 | 769 | } | |
| 623 | 770 |
| ... | ... | @@ -689,6 +836,9 @@ func StartApiServer() { | |
| 689 | 836 | ||
| 690 | 837 | // gather metrics in the auth service | |
| 691 | 838 | go metricDrainSub(ctx, db, logger, cfg.Secret) | |
| 839 | + | // convert container logs to access logs | |
| 840 | + | go containerDrainSub(ctx, db, logger) | |
| 841 | + | ||
| 692 | 842 | defer ctx.Done() | |
| 693 | 843 | ||
| 694 | 844 | apiConfig := &shared.ApiConfig{ |
+40
-0
caddy.json
#
| ... | ... | @@ -0,0 +1,40 @@ | |
| 1 | + | { | |
| 2 | + | "level": "info", | |
| 3 | + | "ts": 1731644477.313701, | |
| 4 | + | "logger": "http.log.access", | |
| 5 | + | "msg": "handled request", | |
| 6 | + | "request": { | |
| 7 | + | "remote_ip": "127.0.0.1", | |
| 8 | + | "remote_port": "40400", | |
| 9 | + | "client_ip": "127.0.0.1", | |
| 10 | + | "proto": "HTTP/2.0", | |
| 11 | + | "method": "GET", | |
| 12 | + | "host": "pgs.sh", | |
| 13 | + | "uri": "/", | |
| 14 | + | "headers": { "User-Agent": ["Blackbox Exporter/0.24.0"] }, | |
| 15 | + | "tls": { | |
| 16 | + | "resumed": false, | |
| 17 | + | "version": 772, | |
| 18 | + | "cipher_suite": 4865, | |
| 19 | + | "proto": "h2", | |
| 20 | + | "server_name": "pgs.sh" | |
| 21 | + | } | |
| 22 | + | }, | |
| 23 | + | "bytes_read": 0, | |
| 24 | + | "user_id": "", | |
| 25 | + | "duration": 0.001207084, | |
| 26 | + | "size": 3718, | |
| 27 | + | "status": 200, | |
| 28 | + | "resp_headers": { | |
| 29 | + | "Referrer-Policy": ["no-referrer-when-downgrade"], | |
| 30 | + | "Strict-Transport-Security": ["max-age=31536000;"], | |
| 31 | + | "X-Content-Type-Options": ["nosniff"], | |
| 32 | + | "X-Frame-Options": ["DENY"], | |
| 33 | + | "Server": ["Caddy"], | |
| 34 | + | "Alt-Svc": ["h3=\":443\"; ma=2592000"], | |
| 35 | + | "Date": ["Fri, 15 Nov 2024 04:21:17 GMT"], | |
| 36 | + | "Content-Type": ["text/html; charset=utf-8"], | |
| 37 | + | "X-Xss-Protection": ["1; mode=block"], | |
| 38 | + | "Permissions-Policy": ["interest-cohort=()"] | |
| 39 | + | } | |
| 40 | + | } |
+12
-11
db/db.go
#
| ... | ... | @@ -161,17 +161,18 @@ type PostAnalytics struct { | |
| 161 | 161 | } | |
| 162 | 162 | ||
| 163 | 163 | type AnalyticsVisits struct { | |
| 164 | - | ID string `json:"id"` | |
| 165 | - | UserID string `json:"user_id"` | |
| 166 | - | ProjectID string `json:"project_id"` | |
| 167 | - | PostID string `json:"post_id"` | |
| 168 | - | Namespace string `json:"namespace"` | |
| 169 | - | Host string `json:"host"` | |
| 170 | - | Path string `json:"path"` | |
| 171 | - | IpAddress string `json:"ip_address"` | |
| 172 | - | UserAgent string `json:"user_agent"` | |
| 173 | - | Referer string `json:"referer"` | |
| 174 | - | Status int `json:"status"` | |
| 164 | + | ID string `json:"id"` | |
| 165 | + | UserID string `json:"user_id"` | |
| 166 | + | ProjectID string `json:"project_id"` | |
| 167 | + | PostID string `json:"post_id"` | |
| 168 | + | Namespace string `json:"namespace"` | |
| 169 | + | Host string `json:"host"` | |
| 170 | + | Path string `json:"path"` | |
| 171 | + | IpAddress string `json:"ip_address"` | |
| 172 | + | UserAgent string `json:"user_agent"` | |
| 173 | + | Referer string `json:"referer"` | |
| 174 | + | Status int `json:"status"` | |
| 175 | + | ContentType string `json:"content_type"` | |
| 175 | 176 | } | |
| 176 | 177 | ||
| 177 | 178 | type VisitInterval struct { |
+2
-1
db/postgres/storage.go
#
| ... | ... | @@ -986,7 +986,7 @@ func newNullString(s string) sql.NullString { | |
| 986 | 986 | ||
| 987 | 987 | func (me *PsqlDB) InsertVisit(visit *db.AnalyticsVisits) error { | |
| 988 | 988 | _, err := me.Db.Exec( | |
| 989 | - | `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);`, | |
| 989 | + | `INSERT INTO analytics_visits (user_id, project_id, post_id, namespace, host, path, ip_address, user_agent, referer, status, content_type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);`, | |
| 990 | 990 | visit.UserID, | |
| 991 | 991 | newNullString(visit.ProjectID), | |
| 992 | 992 | newNullString(visit.PostID), |
+0
-2
imgs/api.go
#
| ... | ... | @@ -177,7 +177,6 @@ func ImgRequest(w http.ResponseWriter, r *http.Request) { | |
| 177 | 177 | dbpool := shared.GetDB(r) | |
| 178 | 178 | logger := shared.GetLogger(r) | |
| 179 | 179 | username := shared.GetUsernameFromRequest(r) | |
| 180 | - | analytics := shared.GetAnalyticsQueue(r) | |
| 181 | 180 | ||
| 182 | 181 | user, err := dbpool.FindUserForName(username) | |
| 183 | 182 | if err != nil { |
+0
-5
pastes/api.go
#
+3
-7
pgs/ssh.go
#
| ... | ... | @@ -11,7 +11,6 @@ import ( | |
| 11 | 11 | "github.com/charmbracelet/promwish" | |
| 12 | 12 | "github.com/charmbracelet/ssh" | |
| 13 | 13 | "github.com/charmbracelet/wish" | |
| 14 | - | "github.com/picosh/pico/db" | |
| 15 | 14 | "github.com/picosh/pico/db/postgres" | |
| 16 | 15 | "github.com/picosh/pico/shared" | |
| 17 | 16 | "github.com/picosh/pico/shared/storage" |
| ... | ... | @@ -81,13 +80,10 @@ func StartSshServer() { | |
| 81 | 80 | st, | |
| 82 | 81 | ) | |
| 83 | 82 | ||
| 84 | - | ch := make(chan *db.AnalyticsVisits, 100) | |
| 85 | - | go shared.AnalyticsCollect(ch, dbpool, logger) | |
| 86 | 83 | apiConfig := &shared.ApiConfig{ | |
| 87 | - | Cfg: cfg, | |
| 88 | - | Dbpool: dbpool, | |
| 89 | - | Storage: st, | |
| 90 | - | AnalyticsQueue: ch, | |
| 84 | + | Cfg: cfg, | |
| 85 | + | Dbpool: dbpool, | |
| 86 | + | Storage: st, | |
| 91 | 87 | } | |
| 92 | 88 | ||
| 93 | 89 | webTunnel := &tunkit.WebTunnelHandler{ |
+1
-2
pgs/tunnel.go
#
| ... | ... | @@ -51,7 +51,7 @@ func createHttpHandler(apiConfig *shared.ApiConfig) CtxHttpBridge { | |
| 51 | 51 | "pubkey", pubkeyStr, | |
| 52 | 52 | ) | |
| 53 | 53 | ||
| 54 | - | props, err := getProjectFromSubdomain(subdomain) | |
| 54 | + | props, err := shared.GetProjectFromSubdomain(subdomain) | |
| 55 | 55 | if err != nil { | |
| 56 | 56 | log.Error(err.Error()) | |
| 57 | 57 | return http.HandlerFunc(shared.UnauthorizedHandler) |
+14
-36
pgs/web.go
#
| ... | ... | @@ -40,10 +40,7 @@ func StartApiServer() { | |
| 40 | 40 | return | |
| 41 | 41 | } | |
| 42 | 42 | ||
| 43 | - | ch := make(chan *db.AnalyticsVisits, 100) | |
| 44 | - | go shared.AnalyticsCollect(ch, dbpool, logger) | |
| 45 | - | ||
| 46 | - | routes := NewWebRouter(cfg, logger, dbpool, st, ch) | |
| 43 | + | routes := NewWebRouter(cfg, logger, dbpool, st) | |
| 47 | 44 | ||
| 48 | 45 | portStr := fmt.Sprintf(":%s", cfg.Port) | |
| 49 | 46 | logger.Info( |
| ... | ... | @@ -61,22 +58,20 @@ func StartApiServer() { | |
| 61 | 58 | type HasPerm = func(proj *db.Project) bool | |
| 62 | 59 | ||
| 63 | 60 | type WebRouter struct { | |
| 64 | - | Cfg *shared.ConfigSite | |
| 65 | - | Logger *slog.Logger | |
| 66 | - | Dbpool db.DB | |
| 67 | - | Storage storage.StorageServe | |
| 68 | - | AnalyticsQueue chan *db.AnalyticsVisits | |
| 69 | - | RootRouter *http.ServeMux | |
| 70 | - | UserRouter *http.ServeMux | |
| 61 | + | Cfg *shared.ConfigSite | |
| 62 | + | Logger *slog.Logger | |
| 63 | + | Dbpool db.DB | |
| 64 | + | Storage storage.StorageServe | |
| 65 | + | RootRouter *http.ServeMux | |
| 66 | + | UserRouter *http.ServeMux | |
| 71 | 67 | } | |
| 72 | 68 | ||
| 73 | - | func NewWebRouter(cfg *shared.ConfigSite, logger *slog.Logger, dbpool db.DB, st storage.StorageServe, analytics chan *db.AnalyticsVisits) *WebRouter { | |
| 69 | + | func NewWebRouter(cfg *shared.ConfigSite, logger *slog.Logger, dbpool db.DB, st storage.StorageServe) *WebRouter { | |
| 74 | 70 | router := &WebRouter{ | |
| 75 | - | Cfg: cfg, | |
| 76 | - | Logger: logger, | |
| 77 | - | Dbpool: dbpool, | |
| 78 | - | Storage: st, | |
| 79 | - | AnalyticsQueue: analytics, | |
| 71 | + | Cfg: cfg, | |
| 72 | + | Logger: logger, | |
| 73 | + | Dbpool: dbpool, | |
| 74 | + | Storage: st, | |
| 80 | 75 | } | |
| 81 | 76 | router.initRouters() | |
| 82 | 77 | return router |
| ... | ... | @@ -177,7 +172,7 @@ func (web *WebRouter) checkHandler(w http.ResponseWriter, r *http.Request) { | |
| 177 | 172 | ||
| 178 | 173 | if !strings.Contains(hostDomain, appDomain) { | |
| 179 | 174 | subdomain := shared.GetCustomDomain(hostDomain, cfg.Space) | |
| 180 | - | props, err := getProjectFromSubdomain(subdomain) | |
| 175 | + | props, err := shared.GetProjectFromSubdomain(subdomain) | |
| 181 | 176 | if err != nil { | |
| 182 | 177 | logger.Error( | |
| 183 | 178 | "could not get project from subdomain", |
| ... | ... | @@ -333,7 +328,7 @@ func (web *WebRouter) ServeAsset(fname string, opts *storage.ImgProcessOpts, fro | |
| 333 | 328 | "host", r.Host, | |
| 334 | 329 | ) | |
| 335 | 330 | ||
| 336 | - | props, err := getProjectFromSubdomain(subdomain) | |
| 331 | + | props, err := shared.GetProjectFromSubdomain(subdomain) | |
| 337 | 332 | if err != nil { | |
| 338 | 333 | logger.Info( | |
| 339 | 334 | "could not determine project from subdomain", |
| ... | ... | @@ -450,20 +445,3 @@ func (web *WebRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| 450 | 445 | ctx = context.WithValue(ctx, shared.CtxSubdomainKey{}, subdomain) | |
| 451 | 446 | router.ServeHTTP(w, r.WithContext(ctx)) | |
| 452 | 447 | } | |
| 453 | - | ||
| 454 | - | type SubdomainProps struct { | |
| 455 | - | ProjectName string | |
| 456 | - | Username string | |
| 457 | - | } | |
| 458 | - | ||
| 459 | - | func getProjectFromSubdomain(subdomain string) (*SubdomainProps, error) { | |
| 460 | - | props := &SubdomainProps{} | |
| 461 | - | strs := strings.SplitN(subdomain, "-", 2) | |
| 462 | - | props.Username = strs[0] | |
| 463 | - | if len(strs) == 2 { | |
| 464 | - | props.ProjectName = strs[1] | |
| 465 | - | } else { | |
| 466 | - | props.ProjectName = props.Username | |
| 467 | - | } | |
| 468 | - | return props, nil | |
| 469 | - | } |
+0
-37
pgs/web_asset_handler.go
#
| ... | ... | @@ -155,22 +153,6 @@ func (h *ApiAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| 155 | 153 | "routes", strings.Join(attempts, ", "), | |
| 156 | 154 | "status", http.StatusNotFound, | |
| 157 | 155 | ) | |
| 158 | - | // track 404s | |
| 159 | - | ch := h.AnalyticsQueue | |
| 160 | - | view, err := shared.AnalyticsVisitFromRequest(r, h.Dbpool, h.UserID) | |
| 161 | - | if err == nil { | |
| 162 | - | view.ProjectID = h.ProjectID | |
| 163 | - | view.Status = http.StatusNotFound | |
| 164 | - | select { | |
| 165 | - | case ch <- view: | |
| 166 | - | default: | |
| 167 | - | logger.Error("could not send analytics view to channel", "view", view) | |
| 168 | - | } | |
| 169 | - | } else { | |
| 170 | - | if !errors.Is(err, shared.ErrAnalyticsDisabled) { | |
| 171 | - | logger.Error("could not record analytics view", "err", err, "view", view) | |
| 172 | - | } | |
| 173 | - | } | |
| 174 | 156 | http.Error(w, "404 not found", http.StatusNotFound) | |
| 175 | 157 | return | |
| 176 | 158 | } |
| ... | ... | @@ -236,25 +218,6 @@ func (h *ApiAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| 236 | 218 | ||
| 237 | 219 | finContentType := w.Header().Get("content-type") | |
| 238 | 220 | ||
| 239 | - | // only track pages, not individual assets | |
| 240 | - | if finContentType == "text/html" { | |
| 241 | - | // track visit | |
| 242 | - | ch := h.AnalyticsQueue | |
| 243 | - | view, err := shared.AnalyticsVisitFromRequest(r, h.Dbpool, h.UserID) | |
| 244 | - | if err == nil { | |
| 245 | - | view.ProjectID = h.ProjectID | |
| 246 | - | select { | |
| 247 | - | case ch <- view: | |
| 248 | - | default: | |
| 249 | - | logger.Error("could not send analytics view to channel", "view", view) | |
| 250 | - | } | |
| 251 | - | } else { | |
| 252 | - | if !errors.Is(err, shared.ErrAnalyticsDisabled) { | |
| 253 | - | logger.Error("could not record analytics view", "err", err, "view", view) | |
| 254 | - | } | |
| 255 | - | } | |
| 256 | - | } | |
| 257 | - | ||
| 258 | 221 | logger.Info( | |
| 259 | 222 | "serving asset", | |
| 260 | 223 | "asset", assetFilepath, |
+2
-42
pgs/web_test.go
#
| ... | ... | @@ -219,8 +218,7 @@ func TestApiBasic(t *testing.T) { | |
| 219 | 218 | responseRecorder := httptest.NewRecorder() | |
| 220 | 219 | ||
| 221 | 220 | st, _ := storage.NewStorageMemory(tc.storage) | |
| 222 | - | ch := make(chan *db.AnalyticsVisits, 100) | |
| 223 | - | router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st, ch) | |
| 221 | + | router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st) | |
| 224 | 222 | router.ServeHTTP(responseRecorder, request) | |
| 225 | 223 | ||
| 226 | 224 | if responseRecorder.Code != tc.status { |
| ... | ... | @@ -240,43 +238,6 @@ func TestApiBasic(t *testing.T) { | |
| 240 | 238 | } | |
| 241 | 239 | } | |
| 242 | 240 | ||
| 243 | - | func TestAnalytics(t *testing.T) { | |
| 244 | - | bucketName := shared.GetAssetBucketName(testUserID) | |
| 245 | - | cfg := NewConfigSite() | |
| 246 | - | cfg.Domain = "pgs.test" | |
| 247 | - | expectedPath := "/app" | |
| 248 | - | request := httptest.NewRequest("GET", mkpath(expectedPath), strings.NewReader("")) | |
| 249 | - | responseRecorder := httptest.NewRecorder() | |
| 250 | - | ||
| 251 | - | sto := map[string]map[string]string{ | |
| 252 | - | bucketName: { | |
| 253 | - | "test/app.html": "hello world!", | |
| 254 | - | }, | |
| 255 | - | } | |
| 256 | - | st, _ := storage.NewStorageMemory(sto) | |
| 257 | - | ch := make(chan *db.AnalyticsVisits, 100) | |
| 258 | - | dbpool := NewPgsAnalticsDb(cfg.Logger) | |
| 259 | - | router := NewWebRouter(cfg, cfg.Logger, dbpool, st, ch) | |
| 260 | - | ||
| 261 | - | go func() { | |
| 262 | - | for analytics := range ch { | |
| 263 | - | if analytics.Path != expectedPath { | |
| 264 | - | t.Errorf("Want path '%s', got '%s'", expectedPath, analytics.Path) | |
| 265 | - | } | |
| 266 | - | close(ch) | |
| 267 | - | } | |
| 268 | - | }() | |
| 269 | - | ||
| 270 | - | router.ServeHTTP(responseRecorder, request) | |
| 271 | - | ||
| 272 | - | select { | |
| 273 | - | case <-ch: | |
| 274 | - | return | |
| 275 | - | case <-time.After(time.Second * 1): | |
| 276 | - | t.Error("didnt receive analytics event within time limit") | |
| 277 | - | } | |
| 278 | - | } | |
| 279 | - | ||
| 280 | 241 | type ImageStorageMemory struct { | |
| 281 | 242 | *storage.StorageMemory | |
| 282 | 243 | Opts *storage.ImgProcessOpts |
| ... | ... | @@ -337,8 +298,7 @@ func TestImageManipulation(t *testing.T) { | |
| 337 | 298 | Ratio: &storage.Ratio{}, | |
| 338 | 299 | }, | |
| 339 | 300 | } | |
| 340 | - | ch := make(chan *db.AnalyticsVisits, 100) | |
| 341 | - | router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st, ch) | |
| 301 | + | router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st) | |
| 342 | 302 | router.ServeHTTP(responseRecorder, request) | |
| 343 | 303 | ||
| 344 | 304 | if responseRecorder.Code != tc.status { |
+3
-43
prose/api.go
#
| ... | ... | @@ -270,21 +264,6 @@ func blogHandler(w http.ResponseWriter, r *http.Request) { | |
| 270 | 264 | postCollection = append(postCollection, p) | |
| 271 | 265 | } | |
| 272 | 266 | ||
| 273 | - | // track visit | |
| 274 | - | ch := shared.GetAnalyticsQueue(r) | |
| 275 | - | view, err := shared.AnalyticsVisitFromRequest(r, dbpool, user.ID) | |
| 276 | - | if err == nil { | |
| 277 | - | select { | |
| 278 | - | case ch <- view: | |
| 279 | - | default: | |
| 280 | - | logger.Error("could not send analytics view to channel", "view", view) | |
| 281 | - | } | |
| 282 | - | } else { | |
| 283 | - | if !errors.Is(err, shared.ErrAnalyticsDisabled) { | |
| 284 | - | logger.Error("could not record analytics view", "err", err, "view", view) | |
| 285 | - | } | |
| 286 | - | } | |
| 287 | - | ||
| 288 | 267 | data := BlogPageData{ | |
| 289 | 268 | Site: *cfg.GetSiteData(), | |
| 290 | 269 | PageTitle: headerTxt.Title, |
| ... | ... | @@ -350,7 +329,6 @@ func postHandler(w http.ResponseWriter, r *http.Request) { | |
| 350 | 329 | username := shared.GetUsernameFromRequest(r) | |
| 351 | 330 | subdomain := shared.GetSubdomain(r) | |
| 352 | 331 | cfg := shared.GetCfg(r) | |
| 353 | - | ch := shared.GetAnalyticsQueue(r) | |
| 354 | 332 | ||
| 355 | 333 | var slug string | |
| 356 | 334 | if !cfg.IsSubdomains() || subdomain == "" { |
| ... | ... | @@ -429,21 +407,6 @@ func postHandler(w http.ResponseWriter, r *http.Request) { | |
| 429 | 407 | ogImageCard = parsedText.ImageCard | |
| 430 | 408 | } | |
| 431 | 409 | ||
| 432 | - | // track visit | |
| 433 | - | view, err := shared.AnalyticsVisitFromRequest(r, dbpool, user.ID) | |
| 434 | - | if err == nil { | |
| 435 | - | view.PostID = post.ID | |
| 436 | - | select { | |
| 437 | - | case ch <- view: | |
| 438 | - | default: | |
| 439 | - | logger.Error("could not send analytics view to channel", "view", view) | |
| 440 | - | } | |
| 441 | - | } else { | |
| 442 | - | if !errors.Is(err, shared.ErrAnalyticsDisabled) { | |
| 443 | - | logger.Error("could not record analytics view", "err", err, "view", view) | |
| 444 | - | } | |
| 445 | - | } | |
| 446 | - | ||
| 447 | 410 | unlisted := false | |
| 448 | 411 | if post.Hidden || post.PublishAt.After(time.Now()) { | |
| 449 | 412 | unlisted = true |
| ... | ... | @@ -953,13 +916,10 @@ func StartApiServer() { | |
| 953 | 916 | mainRoutes := createMainRoutes(staticRoutes) | |
| 954 | 917 | subdomainRoutes := createSubdomainRoutes(staticRoutes) | |
| 955 | 918 | ||
| 956 | - | ch := make(chan *db.AnalyticsVisits, 100) | |
| 957 | - | go shared.AnalyticsCollect(ch, dbpool, logger) | |
| 958 | 919 | apiConfig := &shared.ApiConfig{ | |
| 959 | - | Cfg: cfg, | |
| 960 | - | Dbpool: dbpool, | |
| 961 | - | Storage: st, | |
| 962 | - | AnalyticsQueue: ch, | |
| 920 | + | Cfg: cfg, | |
| 921 | + | Dbpool: dbpool, | |
| 922 | + | Storage: st, | |
| 963 | 923 | } | |
| 964 | 924 | handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig) | |
| 965 | 925 | router := http.HandlerFunc(handler) |
+1
-0
sql/migrations/20241125_add_content_type_to_analytics.sql
#
| ... | ... | @@ -0,0 +1,1 @@ | |
| 1 | + | ALTER TABLE analytics_visits ADD COLUMN content_type varchar(256); |
+0
-0
test.txt
#
Binaries are not rendered as diffs.