From 3b203d9a481e884a8b19fc7e7e56abaf96e86469 Mon Sep 17 00:00:00 2001 From: Ralf Grubenmann Date: Wed, 26 Mar 2025 16:27:17 +0100 Subject: [PATCH 1/4] feat: add login product metrics --- cmd/gateway/main.go | 14 +++++++++- go.mod | 1 + go.sum | 2 ++ internal/config/main.go | 1 + internal/config/other.go | 6 +++++ internal/login/login_server.go | 8 ++++++ internal/login/login_server_routes.go | 6 +++++ internal/metrics/posthog.go | 39 +++++++++++++++++++++++++++ internal/models/metrics.go | 5 ++++ 9 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 internal/metrics/posthog.go create mode 100644 internal/models/metrics.go diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index ebbc4707..d0c30c26 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,18 @@ func main() { } revproxy.RegisterHandlers(e, gwMiddlewares...) // Initialize login server - loginServer, err := login.NewLoginServer(login.WithConfig(gwConfig.Login), login.WithSessionStore(sessionStore), login.WithTokenStore(tokenStore)) + loginOptions := []login.LoginServerOption{login.WithConfig(gwConfig.Login), login.WithSessionStore(sessionStore), login.WithTokenStore(tokenStore)} + + if gwConfig.Posthog.Enabled { + metricsClient, err := metrics.NewPosthogClient(gwConfig.Posthog.ApiKey, gwConfig.Posthog.Host) + if err != nil { + slog.Error("posthog client initializtion failed", "error", err) + os.Exit(1) + } + loginOptions = append(loginOptions, login.WithMetricsClient(metricsClient)) + defer metricsClient.Close() + } + loginServer, err := login.NewLoginServer(loginOptions...) 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..228f1afe 100644 --- a/internal/config/other.go +++ b/internal/config/other.go @@ -29,3 +29,9 @@ type RateLimits struct { Rate float64 Burst int } + +type PosthogConfig struct { + Enabled bool + ApiKey string + Host 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..4fbf162f --- /dev/null +++ b/internal/metrics/posthog.go @@ -0,0 +1,39 @@ +package metrics + +import ( + "crypto/md5" + "encoding/hex" + + "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(apiKey string, host string) (*PosthogMetricsClient, error) { + client, err := posthog.NewWithConfig( + apiKey, + posthog.Config{ + Endpoint: host, + }, + ) + if err != nil { + return &PosthogMetricsClient{}, 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 +} From 18a0bd24805d7970ecc1e2efd77d21008ea6fe07 Mon Sep 17 00:00:00 2001 From: Ralf Grubenmann Date: Mon, 31 Mar 2025 15:27:44 +0200 Subject: [PATCH 2/4] add environment configuration to posthog --- cmd/gateway/main.go | 2 +- internal/config/other.go | 7 ++++--- internal/metrics/posthog.go | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index d0c30c26..5b85cd8c 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -128,7 +128,7 @@ func main() { loginOptions := []login.LoginServerOption{login.WithConfig(gwConfig.Login), login.WithSessionStore(sessionStore), login.WithTokenStore(tokenStore)} if gwConfig.Posthog.Enabled { - metricsClient, err := metrics.NewPosthogClient(gwConfig.Posthog.ApiKey, gwConfig.Posthog.Host) + metricsClient, err := metrics.NewPosthogClient(gwConfig.Posthog.ApiKey, gwConfig.Posthog.Host, gwConfig.Posthog.Environment) if err != nil { slog.Error("posthog client initializtion failed", "error", err) os.Exit(1) diff --git a/internal/config/other.go b/internal/config/other.go index 228f1afe..0e4b85b1 100644 --- a/internal/config/other.go +++ b/internal/config/other.go @@ -31,7 +31,8 @@ type RateLimits struct { } type PosthogConfig struct { - Enabled bool - ApiKey string - Host string + Enabled bool + ApiKey string + Host string + Environment string } diff --git a/internal/metrics/posthog.go b/internal/metrics/posthog.go index 4fbf162f..372fd12a 100644 --- a/internal/metrics/posthog.go +++ b/internal/metrics/posthog.go @@ -24,11 +24,12 @@ func (p *PosthogMetricsClient) Close() { p.posthogClient.Close() } -func NewPosthogClient(apiKey string, host string) (*PosthogMetricsClient, error) { +func NewPosthogClient(apiKey string, host string, environment string) (*PosthogMetricsClient, error) { client, err := posthog.NewWithConfig( apiKey, posthog.Config{ - Endpoint: host, + Endpoint: host, + DefaultEventProperties: posthog.Properties{"environment": environment}, }, ) if err != nil { From eafd586ab5b571884c166ccea5be0edf0fd311df Mon Sep 17 00:00:00 2001 From: Ralf Grubenmann Date: Thu, 24 Apr 2025 08:48:04 +0200 Subject: [PATCH 3/4] address comment --- cmd/gateway/main.go | 14 +++++++------- internal/config/other.go | 2 +- internal/metrics/posthog.go | 9 +++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index 5b85cd8c..a900947b 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -127,15 +127,15 @@ func main() { // Initialize login server loginOptions := []login.LoginServerOption{login.WithConfig(gwConfig.Login), login.WithSessionStore(sessionStore), login.WithTokenStore(tokenStore)} - if gwConfig.Posthog.Enabled { - metricsClient, err := metrics.NewPosthogClient(gwConfig.Posthog.ApiKey, gwConfig.Posthog.Host, gwConfig.Posthog.Environment) - if err != nil { - slog.Error("posthog client initializtion failed", "error", err) - os.Exit(1) - } - loginOptions = append(loginOptions, login.WithMetricsClient(metricsClient)) + 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() } + loginOptions = append(loginOptions, login.WithMetricsClient(metricsClient)) loginServer, err := login.NewLoginServer(loginOptions...) if err != nil { slog.Error("login handlers initialization failed", "error", err) diff --git a/internal/config/other.go b/internal/config/other.go index 0e4b85b1..3ee5d78b 100644 --- a/internal/config/other.go +++ b/internal/config/other.go @@ -32,7 +32,7 @@ type RateLimits struct { type PosthogConfig struct { Enabled bool - ApiKey string + ApiKey RedactedString Host string Environment string } diff --git a/internal/metrics/posthog.go b/internal/metrics/posthog.go index 372fd12a..6ac90a00 100644 --- a/internal/metrics/posthog.go +++ b/internal/metrics/posthog.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "encoding/hex" + "github.com/SwissDataScienceCenter/renku-gateway/internal/config" "github.com/posthog/posthog-go" ) @@ -24,12 +25,12 @@ func (p *PosthogMetricsClient) Close() { p.posthogClient.Close() } -func NewPosthogClient(apiKey string, host string, environment string) (*PosthogMetricsClient, error) { +func NewPosthogClient(c config.PosthogConfig) (*PosthogMetricsClient, error) { client, err := posthog.NewWithConfig( - apiKey, + string(c.ApiKey), posthog.Config{ - Endpoint: host, - DefaultEventProperties: posthog.Properties{"environment": environment}, + Endpoint: c.Host, + DefaultEventProperties: posthog.Properties{"environment": c.Environment}, }, ) if err != nil { From 89d45ed1e14704c6f350f715b3eb69848420ed69 Mon Sep 17 00:00:00 2001 From: Ralf Grubenmann Date: Wed, 7 May 2025 09:38:26 +0200 Subject: [PATCH 4/4] return nil --- cmd/gateway/main.go | 10 ++++++---- internal/metrics/posthog.go | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index a900947b..00ee888d 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -125,8 +125,6 @@ func main() { } revproxy.RegisterHandlers(e, gwMiddlewares...) // Initialize login server - loginOptions := []login.LoginServerOption{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) @@ -135,8 +133,12 @@ func main() { if metricsClient != nil { defer metricsClient.Close() } - loginOptions = append(loginOptions, login.WithMetricsClient(metricsClient)) - loginServer, err := login.NewLoginServer(loginOptions...) + 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/internal/metrics/posthog.go b/internal/metrics/posthog.go index 6ac90a00..7a4e15a7 100644 --- a/internal/metrics/posthog.go +++ b/internal/metrics/posthog.go @@ -26,6 +26,9 @@ func (p *PosthogMetricsClient) Close() { } func NewPosthogClient(c config.PosthogConfig) (*PosthogMetricsClient, error) { + if !c.Enabled { + return nil, nil + } client, err := posthog.NewWithConfig( string(c.ApiKey), posthog.Config{ @@ -34,7 +37,7 @@ func NewPosthogClient(c config.PosthogConfig) (*PosthogMetricsClient, error) { }, ) if err != nil { - return &PosthogMetricsClient{}, err + return nil, err } return &PosthogMetricsClient{posthogClient: client}, nil