diff --git a/appview/config.go b/appview/config.go index c4ca330..2311309 100644 --- a/appview/config.go +++ b/appview/config.go @@ -36,10 +36,16 @@ type AvatarConfig struct { SharedSecret string `env:"SHARED_SECRET"` } +type PosthogConfig struct { + ApiKey string `env:"API_KEY"` + Endpoint string `env:"ENDPOINT, default=https://eu.i.posthog.com"` +} + type Config struct { Core CoreConfig `env:",prefix=TANGLED_"` Jetstream JetstreamConfig `env:",prefix=TANGLED_JETSTREAM_"` Resend ResendConfig `env:",prefix=TANGLED_RESEND_"` + Posthog PosthogConfig `env:",prefix=TANGLED_POSTHOG_"` Camo CamoConfig `env:",prefix=TANGLED_CAMO_"` Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"` OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"` diff --git a/appview/oauth/handler/handler.go b/appview/oauth/handler/handler.go index dbdfcfc..79ac43f 100644 --- a/appview/oauth/handler/handler.go +++ b/appview/oauth/handler/handler.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/sessions" "github.com/haileyok/atproto-oauth-golang/helpers" "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/posthog/posthog-go" "tangled.sh/tangled.sh/core/appview" "tangled.sh/tangled.sh/core/appview/db" "tangled.sh/tangled.sh/core/appview/knotclient" @@ -34,6 +35,7 @@ type OAuthHandler struct { Store *sessions.CookieStore OAuth *oauth.OAuth Enforcer *rbac.Enforcer + Posthog posthog.Client } func (o *OAuthHandler) Router() http.Handler { @@ -248,6 +250,16 @@ func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) { log.Println("session saved successfully") go o.addToDefaultKnot(oauthRequest.Did) + if !o.Config.Core.Dev { + err = o.Posthog.Enqueue(posthog.Capture{ + DistinctId: oauthRequest.Did, + Event: "signin", + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + http.Redirect(w, r, "/", http.StatusFound) } diff --git a/appview/state/follow.go b/appview/state/follow.go index 4a99f8b..335472a 100644 --- a/appview/state/follow.go +++ b/appview/state/follow.go @@ -7,6 +7,7 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" lexutil "github.com/bluesky-social/indigo/lex/util" + "github.com/posthog/posthog-go" "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/appview" "tangled.sh/tangled.sh/core/appview/db" @@ -70,6 +71,17 @@ func (s *State) Follow(w http.ResponseWriter, r *http.Request) { FollowStatus: db.IsFollowing, }) + if !s.config.Core.Dev { + err = s.posthog.Enqueue(posthog.Capture{ + DistinctId: currentUser.Did, + Event: "follow", + Properties: posthog.Properties{"subject": subjectIdent.DID.String()}, + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + return case http.MethodDelete: // find the record in the db @@ -101,6 +113,17 @@ func (s *State) Follow(w http.ResponseWriter, r *http.Request) { FollowStatus: db.IsNotFollowing, }) + if !s.config.Core.Dev { + err = s.posthog.Enqueue(posthog.Capture{ + DistinctId: currentUser.Did, + Event: "unfollow", + Properties: posthog.Properties{"subject": subjectIdent.DID.String()}, + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + return } diff --git a/appview/state/profile.go b/appview/state/profile.go index a5b5136..9a00530 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -15,6 +15,7 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" "github.com/go-chi/chi/v5" + "github.com/posthog/posthog-go" "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/appview/db" "tangled.sh/tangled.sh/core/appview/pages" @@ -347,6 +348,16 @@ func (s *State) updateProfile(profile *db.Profile, w http.ResponseWriter, r *htt return } + if !s.config.Core.Dev { + err = s.posthog.Enqueue(posthog.Capture{ + DistinctId: user.Did, + Event: "edit_profile", + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + s.pages.HxRedirect(w, "/"+user.Did) return } diff --git a/appview/state/pull.go b/appview/state/pull.go index 35c17c8..e251636 100644 --- a/appview/state/pull.go +++ b/appview/state/pull.go @@ -28,6 +28,7 @@ import ( lexutil "github.com/bluesky-social/indigo/lex/util" "github.com/go-chi/chi/v5" "github.com/google/uuid" + "github.com/posthog/posthog-go" ) // htmx fragment @@ -605,6 +606,17 @@ func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { return } + if !s.config.Core.Dev { + err = s.posthog.Enqueue(posthog.Capture{ + DistinctId: user.Did, + Event: "new_pull_comment", + Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pull.PullId}, + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) return } @@ -982,6 +994,17 @@ func (s *State) createPullRequest( return } + if !s.config.Core.Dev { + err = s.posthog.Enqueue(posthog.Capture{ + DistinctId: user.Did, + Event: "new_pull", + Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pullId}, + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) } diff --git a/appview/state/repo.go b/appview/state/repo.go index 5db1828..c19c4cc 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -33,6 +33,7 @@ import ( securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-chi/chi/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/posthog/posthog-go" comatproto "github.com/bluesky-social/indigo/api/atproto" lexutil "github.com/bluesky-social/indigo/lex/util" @@ -1877,6 +1878,17 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { return } + if !s.config.Core.Dev { + err = s.posthog.Enqueue(posthog.Capture{ + DistinctId: user.Did, + Event: "new_issue", + Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "issue_id": issueId}, + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) return } diff --git a/appview/state/router.go b/appview/state/router.go index 45a3823..9b68010 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -269,6 +269,7 @@ func (s *State) OAuthRouter() http.Handler { Store: sessions.NewCookieStore([]byte(s.config.Core.CookieSecret)), OAuth: s.oauth, Enforcer: s.enforcer, + Posthog: s.posthog, } return oauth.Router() diff --git a/appview/state/star.go b/appview/state/star.go index 7f4635f..8a2b3a8 100644 --- a/appview/state/star.go +++ b/appview/state/star.go @@ -8,6 +8,7 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" + "github.com/posthog/posthog-go" "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/appview" "tangled.sh/tangled.sh/core/appview/db" @@ -75,6 +76,17 @@ func (s *State) Star(w http.ResponseWriter, r *http.Request) { }, }) + if !s.config.Core.Dev { + err = s.posthog.Enqueue(posthog.Capture{ + DistinctId: currentUser.Did, + Event: "star", + Properties: posthog.Properties{"repo_at": subjectUri.String()}, + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + return case http.MethodDelete: // find the record in the db @@ -115,6 +127,17 @@ func (s *State) Star(w http.ResponseWriter, r *http.Request) { }, }) + if !s.config.Core.Dev { + err = s.posthog.Enqueue(posthog.Capture{ + DistinctId: currentUser.Did, + Event: "unstar", + Properties: posthog.Properties{"repo_at": subjectUri.String()}, + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + return } diff --git a/appview/state/state.go b/appview/state/state.go index 89b98dc..da50f23 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -17,6 +17,7 @@ import ( lexutil "github.com/bluesky-social/indigo/lex/util" securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-chi/chi/v5" + "github.com/posthog/posthog-go" "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/appview" "tangled.sh/tangled.sh/core/appview/db" @@ -34,6 +35,7 @@ type State struct { tidClock syntax.TIDClock pages *pages.Pages resolver *appview.Resolver + posthog posthog.Client jc *jetstream.JetstreamClient config *appview.Config } @@ -57,6 +59,11 @@ func Make(config *appview.Config) (*State, error) { oauth := oauth.NewOAuth(d, config) + posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint}) + if err != nil { + return nil, fmt.Errorf("failed to create posthog client: %w", err) + } + wrapper := db.DbWrapper{d} jc, err := jetstream.NewJetstreamClient( config.Jetstream.Endpoint, @@ -88,6 +95,7 @@ func Make(config *appview.Config) (*State, error) { clock, pgs, resolver, + posthog, jc, config, } @@ -99,82 +107,6 @@ func TID(c *syntax.TIDClock) string { return c.Next().String() } -// func (s *State) Login(w http.ResponseWriter, r *http.Request) { -// ctx := r.Context() - -// switch r.Method { -// case http.MethodGet: -// err := s.pages.Login(w, pages.LoginParams{}) -// if err != nil { -// log.Printf("rendering login page: %s", err) -// } - -// return -// case http.MethodPost: -// handle := strings.TrimPrefix(r.FormValue("handle"), "@") -// appPassword := r.FormValue("app_password") - -// resolved, err := s.resolver.ResolveIdent(ctx, handle) -// if err != nil { -// log.Println("failed to resolve handle:", err) -// s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) -// return -// } - -// atSession, err := s.oauth.CreateInitialSession(ctx, resolved, appPassword) -// if err != nil { -// s.pages.Notice(w, "login-msg", "Invalid handle or password.") -// return -// } -// sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} - -// err = s.oauth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) -// if err != nil { -// s.pages.Notice(w, "login-msg", "Failed to login, try again later.") -// return -// } - -// log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) - -// did := resolved.DID.String() -// defaultKnot := "knot1.tangled.sh" - -// go func() { -// log.Printf("adding %s to default knot", did) -// err = s.enforcer.AddMember(defaultKnot, did) -// if err != nil { -// log.Println("failed to add user to knot1.tangled.sh: ", err) -// return -// } -// err = s.enforcer.E.SavePolicy() -// if err != nil { -// log.Println("failed to add user to knot1.tangled.sh: ", err) -// return -// } - -// secret, err := db.GetRegistrationKey(s.db, defaultKnot) -// if err != nil { -// log.Println("failed to get registration key for knot1.tangled.sh") -// return -// } -// signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Core.Dev) -// resp, err := signedClient.AddMember(did) -// if err != nil { -// log.Println("failed to add user to knot1.tangled.sh: ", err) -// return -// } - -// if resp.StatusCode != http.StatusNoContent { -// log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) -// return -// } -// }() - -// s.pages.HxRedirect(w, "/") -// return -// } -// } - func (s *State) Logout(w http.ResponseWriter, r *http.Request) { s.oauth.ClearSession(r, w) w.Header().Set("HX-Redirect", "/login") @@ -775,6 +707,17 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { return } + if !s.config.Core.Dev { + err = s.posthog.Enqueue(posthog.Capture{ + DistinctId: user.Did, + Event: "new_repo", + Properties: posthog.Properties{"repo": repoName, "repo_at": repo.AtUri}, + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } + } + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) return } diff --git a/go.mod b/go.mod index 1f1a89a..26f0d91 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/lestrrat-go/jwx/v2 v2.0.12 github.com/mattn/go-sqlite3 v1.14.24 github.com/microcosm-cc/bluemonday v1.0.27 + github.com/posthog/posthog-go v1.5.5 github.com/resend/resend-go/v2 v2.15.0 github.com/sethvargo/go-envconfig v1.1.0 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e diff --git a/go.sum b/go.sum index a694cf0..9747a74 100644 --- a/go.sum +++ b/go.sum @@ -224,6 +224,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= +github.com/posthog/posthog-go v1.5.5 h1:2o3j7IrHbTIfxRtj4MPaXKeimuTYg49onNzNBZbwksM= +github.com/posthog/posthog-go v1.5.5/go.mod h1:3RqUmSnPuwmeVj/GYrS75wNGqcAKdpODiwc83xZWgdE= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=