diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index ebbc4707..00ee888d 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -15,6 +15,7 @@ import ( "github.com/SwissDataScienceCenter/renku-gateway/internal/config" "github.com/SwissDataScienceCenter/renku-gateway/internal/db" "github.com/SwissDataScienceCenter/renku-gateway/internal/login" + "github.com/SwissDataScienceCenter/renku-gateway/internal/metrics" "github.com/SwissDataScienceCenter/renku-gateway/internal/revproxy" "github.com/SwissDataScienceCenter/renku-gateway/internal/sessions" "github.com/SwissDataScienceCenter/renku-gateway/internal/tokenstore" @@ -124,7 +125,20 @@ func main() { } revproxy.RegisterHandlers(e, gwMiddlewares...) // Initialize login server - loginServer, err := login.NewLoginServer(login.WithConfig(gwConfig.Login), login.WithSessionStore(sessionStore), login.WithTokenStore(tokenStore)) + metricsClient, err := metrics.NewPosthogClient(gwConfig.Posthog) + if err != nil { + slog.Error("posthog client initializtion failed", "error", err) + os.Exit(1) + } + if metricsClient != nil { + defer metricsClient.Close() + } + loginServer, err := login.NewLoginServer( + login.WithConfig(gwConfig.Login), + login.WithSessionStore(sessionStore), + login.WithTokenStore(tokenStore), + login.WithMetricsClient(metricsClient), + ) if err != nil { slog.Error("login handlers initialization failed", "error", err) os.Exit(1) diff --git a/go.mod b/go.mod index a42577db..f31a7ee5 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/oapi-codegen/runtime v1.1.1 github.com/oklog/ulid/v2 v2.1.0 + github.com/posthog/posthog-go v1.3.3 github.com/redis/go-redis/v9 v9.4.0 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 8131d838..27a7d8be 100644 --- a/go.sum +++ b/go.sum @@ -151,6 +151,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posthog/posthog-go v1.3.3 h1:0b1JlfPDMKXGRgbTtqkSG6hsrlj8yW2tv2HS6Dn7wFk= +github.com/posthog/posthog-go v1.3.3/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= diff --git a/internal/config/main.go b/internal/config/main.go index f0846e35..48e93373 100644 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -8,6 +8,7 @@ type Config struct { Revproxy RevproxyConfig Login LoginConfig Redis RedisConfig + Posthog PosthogConfig Monitoring MonitoringConfig } diff --git a/internal/config/other.go b/internal/config/other.go index bc043c35..3ee5d78b 100644 --- a/internal/config/other.go +++ b/internal/config/other.go @@ -29,3 +29,10 @@ type RateLimits struct { Rate float64 Burst int } + +type PosthogConfig struct { + Enabled bool + ApiKey RedactedString + Host string + Environment string +} diff --git a/internal/login/login_server.go b/internal/login/login_server.go index 2f49ae9b..0679e256 100644 --- a/internal/login/login_server.go +++ b/internal/login/login_server.go @@ -15,6 +15,7 @@ type LoginServer struct { providerStore oidc.ClientStore sessions *sessions.SessionStore tokenStore models.TokenStoreInterface + metricsClient models.MetricsClientInterface } func (l *LoginServer) RegisterHandlers(server *echo.Echo, commonMiddlewares ...echo.MiddlewareFunc) { @@ -58,6 +59,13 @@ func WithTokenStore(store models.TokenStoreInterface) LoginServerOption { } } +func WithMetricsClient(client models.MetricsClientInterface) LoginServerOption { + return func(l *LoginServer) error { + l.metricsClient = client + return nil + } +} + // NewLoginServer creates a new LoginServer that handles the callbacks from oauth2 // and initiates the login flow for users. func NewLoginServer(options ...LoginServerOption) (*LoginServer, error) { diff --git a/internal/login/login_server_routes.go b/internal/login/login_server_routes.go index f1a7251f..2a0d193f 100644 --- a/internal/login/login_server_routes.go +++ b/internal/login/login_server_routes.go @@ -218,6 +218,12 @@ func (l *LoginServer) nextAuthStep( slog.Info("login completed", "requestID", utils.GetRequestID(c), "appRedirectURL", url) // Save the session: ensure we save the session before sending redirects l.sessions.Save(c) + // send product metrics + if l.metricsClient != nil { + if session, err := l.sessions.Get(c); err == nil { + l.metricsClient.UserLoggedIn(session.UserID) + } + } return c.Redirect(http.StatusFound, url) } providerID := session.LoginSequence[0] diff --git a/internal/metrics/posthog.go b/internal/metrics/posthog.go new file mode 100644 index 00000000..7a4e15a7 --- /dev/null +++ b/internal/metrics/posthog.go @@ -0,0 +1,44 @@ +package metrics + +import ( + "crypto/md5" + "encoding/hex" + + "github.com/SwissDataScienceCenter/renku-gateway/internal/config" + "github.com/posthog/posthog-go" +) + +type PosthogMetricsClient struct { + posthogClient posthog.Client +} + +func (p *PosthogMetricsClient) anonymizeUser(userId string) string { + hash := md5.Sum([]byte(userId)) + return hex.EncodeToString(hash[:]) +} + +func (p *PosthogMetricsClient) UserLoggedIn(userId string) error { + return p.posthogClient.Enqueue(posthog.Capture{DistinctId: p.anonymizeUser(userId), Event: "user_logged_in"}) +} + +func (p *PosthogMetricsClient) Close() { + p.posthogClient.Close() +} + +func NewPosthogClient(c config.PosthogConfig) (*PosthogMetricsClient, error) { + if !c.Enabled { + return nil, nil + } + client, err := posthog.NewWithConfig( + string(c.ApiKey), + posthog.Config{ + Endpoint: c.Host, + DefaultEventProperties: posthog.Properties{"environment": c.Environment}, + }, + ) + if err != nil { + return nil, err + } + + return &PosthogMetricsClient{posthogClient: client}, nil +} diff --git a/internal/models/metrics.go b/internal/models/metrics.go new file mode 100644 index 00000000..3d7a6317 --- /dev/null +++ b/internal/models/metrics.go @@ -0,0 +1,5 @@ +package models + +type MetricsClientInterface interface { + UserLoggedIn(userId string) error +}