Kaynağa Gözat

Merge pull request #899 from porter-dev/0.6.0-refactor-segment-implementation

[0.6.0] Refactor segment implementation
abelanger5 4 yıl önce
ebeveyn
işleme
6287486719

+ 45 - 0
docs/developing/analytics.md

@@ -0,0 +1,45 @@
+# How the analytics package works
+
+The analytics package is entirely dependant over segment, to use it you should add
+a config key SEGMENT_CLIENT_KEY on `docker/.env` file.
+To find the segment client key check [this link](https://segment.com/docs/connections/find-writekey/).
+
+This package is divided in four files:
+
+- segment.go
+
+  The _segment.go_ file exports a function to initialize the analytics client, and two superset of the original segment client functions Track and Identify. This functions will handle cases when the segment client is not initialized and will return an error if the client failed enqueueing a certain track/identify.
+
+- tracks.go
+
+  _tracks.go_ will export an interface `SegmentTrack` that all the tracks should follow, this helps when trying to standardize the analytics package. The idea behind this is to always use a constructor for the track that we're trying to use instead of having different implementations all over the app.
+
+- track_events.go
+
+  Enum of events that can be used on tracks, those will be implemented on the tracks.go so they shouldn't appear in any other part of the application.
+
+- identifiers.go
+
+  Similar as the tracks.go, although this is more specialized as it should only be used on user register/login/update parts of the application.
+
+## How to add new analytics to the app
+
+### Adding new segment spec objects
+
+The current implementation only uses [Tracks](https://segment.com/docs/connections/spec/track/) and [Identifiers](https://segment.com/docs/connections/spec/identify/) specs from the segment package, in order to add a new spec you should follow this steps:
+
+- Add the spec function that you want to use on the `internal/analytics/segment.go` file, it should always receive an interface that will get the necessary data for the segment spec function that you want to add.
+- Create a new file on the same `internal/analytics` folder with the name on plural of the spec you want to add.
+- In this spec file, you should declare the interface that the analyticsClient spec function will receive, and after that the correspondant structs that will refer to the different metrics you want to add. For more examples on how to implement this you can use as reference the `internal/analytics/tracks.go` file.
+- Update this file with the correspondant documentation about the implementation
+
+### Adding new objects to current implemented specs
+
+In order to add new metrics to the current implementation the process should be simple:
+
+- Look for the segment spec file in `internal/analytics` folder that you want to use
+- Add a new struct that accomplish the interface defined at the start of the file with the data that you need for that metric
+- Write a constructor for the struct.
+- You're done to use!
+
+For any doubts about this document or how to improve the analytics you can reach us on discord!

+ 48 - 0
internal/analytics/identifiers.go

@@ -0,0 +1,48 @@
+package analytics
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/internal/models"
+	segment "gopkg.in/segmentio/analytics-go.v3"
+)
+
+type segmentIdentifier interface {
+	getUserId() string
+	getTraits() segment.Traits
+}
+
+type segmentIdentifyNewUser struct {
+	userId    string
+	userEmail string
+	isGithub  bool
+}
+
+// Creates a segment Identifier struct for new users. As we handle registration with github, it also accepts a param
+// to check if the new user has registered with github or not.
+func CreateSegmentIdentifyNewUser(user *models.User, registeredViaGithub bool) *segmentIdentifyNewUser {
+	userId := fmt.Sprintf("%v", user.ID)
+	return &segmentIdentifyNewUser{
+		userId:    userId,
+		userEmail: user.Email,
+		isGithub:  registeredViaGithub,
+	}
+}
+
+func (i segmentIdentifyNewUser) getUserId() string {
+	return i.userId
+}
+
+func (i segmentIdentifyNewUser) getTraits() segment.Traits {
+	var githubTrait string
+
+	if i.isGithub {
+		githubTrait = "true"
+	} else {
+		githubTrait = "false"
+	}
+
+	return segment.NewTraits().
+		SetEmail(i.userEmail).
+		Set("github", githubTrait)
+}

+ 77 - 0
internal/analytics/segment.go

@@ -0,0 +1,77 @@
+package analytics
+
+import (
+	"github.com/porter-dev/porter/internal/logger"
+	segment "gopkg.in/segmentio/analytics-go.v3"
+)
+
+type AnalyticsSegmentClient interface {
+	Identify(segmentIdentifier) error
+	Track(segmentTrack) error
+}
+
+type AnalyticsSegment struct {
+	segment.Client
+	isEnabled bool
+	logger    *logger.Logger
+}
+
+// Initialize the segment client and return a superset of it, the AnalyticsSegmentClient will handle cases when
+// the segment client failed on initialization or not enabled
+func InitializeAnalyticsSegmentClient(segmentClientKey string, logger *logger.Logger) AnalyticsSegmentClient {
+	if segmentClientKey != "" {
+
+		client := segment.New(segmentClientKey)
+
+		if client == nil {
+			return &AnalyticsSegment{
+				isEnabled: false,
+				logger:    logger,
+			}
+		}
+
+		return &AnalyticsSegment{
+			Client:    client,
+			isEnabled: true,
+			logger:    logger,
+		}
+	}
+
+	return &AnalyticsSegment{
+		isEnabled: false,
+		logger:    logger,
+	}
+}
+
+//	Superset of segment client identify function, this will accept analytics defined identifiers only
+//	and will log an error if the client is not initialized
+
+func (c *AnalyticsSegment) Identify(identifier segmentIdentifier) error {
+	if !c.isEnabled {
+		c.logger.Error().Msg("Analytics not enabled")
+		return nil
+	}
+
+	err := c.Enqueue(segment.Identify{
+		UserId: identifier.getUserId(),
+		Traits: identifier.getTraits(),
+	})
+	return err
+}
+
+//	Superset of segment client track function, this will accept analytics defined tracks only
+//	and will log an error if the client is not initialized
+func (c *AnalyticsSegment) Track(track segmentTrack) error {
+	if !c.isEnabled {
+		c.logger.Error().Msg("Analytics not enabled")
+		return nil
+	}
+
+	err := c.Enqueue(segment.Track{
+		UserId:     track.getUserId(),
+		Event:      string(track.getEvent()),
+		Properties: track.getProperties(),
+	})
+
+	return err
+}

+ 8 - 0
internal/analytics/track_events.go

@@ -0,0 +1,8 @@
+package analytics
+
+type SegmentEvent string
+
+const (
+	NewUser            SegmentEvent = "New User"
+	RedeployViaWebhook SegmentEvent = "Triggered Re-deploy via Webhook"
+)

+ 68 - 0
internal/analytics/tracks.go

@@ -0,0 +1,68 @@
+package analytics
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/internal/models"
+	segment "gopkg.in/segmentio/analytics-go.v3"
+)
+
+type segmentTrack interface {
+	getUserId() string
+	getEvent() SegmentEvent
+	getProperties() segment.Properties
+}
+
+type segmentNewUserTrack struct {
+	userId    string
+	userEmail string
+}
+
+// Constructor for track of type "New User"
+// Tracks when a user has registered
+func CreateSegmentNewUserTrack(user *models.User) *segmentNewUserTrack {
+	userId := fmt.Sprintf("%v", user.ID)
+
+	return &segmentNewUserTrack{
+		userId:    userId,
+		userEmail: user.Email,
+	}
+}
+
+func (t *segmentNewUserTrack) getUserId() string {
+	return t.userId
+}
+
+func (t *segmentNewUserTrack) getEvent() SegmentEvent {
+	return NewUser
+}
+
+func (t *segmentNewUserTrack) getProperties() segment.Properties {
+	return segment.NewProperties().Set("email", t.userEmail)
+}
+
+type segmentRedeployViaWebhookTrack struct {
+	userId     string
+	repository string
+}
+
+// Constructor for track of type "Triggered Re-deploy via Webhook"
+// tracks whenever a repository is redeployed via webhook call
+func CreateSegmentRedeployViaWebhookTrack(userId string, repository string) *segmentRedeployViaWebhookTrack {
+	return &segmentRedeployViaWebhookTrack{
+		userId:     userId,
+		repository: repository,
+	}
+}
+
+func (t *segmentRedeployViaWebhookTrack) getUserId() string {
+	return t.userId
+}
+
+func (t *segmentRedeployViaWebhookTrack) getEvent() SegmentEvent {
+	return RedeployViaWebhook
+}
+
+func (t *segmentRedeployViaWebhookTrack) getProperties() segment.Properties {
+	return segment.NewProperties().Set("repository", t.repository)
+}

+ 8 - 10
server/api/api.go

@@ -22,9 +22,9 @@ import (
 	"github.com/porter-dev/porter/internal/repository"
 	memory "github.com/porter-dev/porter/internal/repository/memory"
 	"github.com/porter-dev/porter/internal/validator"
-	segment "gopkg.in/segmentio/analytics-go.v3"
 	"helm.sh/helm/v3/pkg/storage"
 
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/config"
 )
 
@@ -87,11 +87,11 @@ type App struct {
 	DOConf            *oauth2.Config
 	GoogleUserConf    *oauth2.Config
 
-	db            *gorm.DB
-	validator     *vr.Validate
-	translator    *ut.Translator
-	tokenConf     *token.TokenGeneratorConf
-	segmentClient *segment.Client
+	db              *gorm.DB
+	validator       *vr.Validate
+	translator      *ut.Translator
+	tokenConf       *token.TokenGeneratorConf
+	analyticsClient analytics.AnalyticsSegmentClient
 }
 
 type AppCapabilities struct {
@@ -211,10 +211,8 @@ func New(conf *AppConfig) (*App, error) {
 		TokenSecret: conf.ServerConf.TokenGeneratorSecret,
 	}
 
-	if sc := conf.ServerConf; sc.SegmentClientKey != "" {
-		client := segment.New(sc.SegmentClientKey)
-		app.segmentClient = &client
-	}
+	newSegmentClient := analytics.InitializeAnalyticsSegmentClient(sc.SegmentClientKey, app.Logger)
+	app.analyticsClient = newSegmentClient
 
 	return app, nil
 }

+ 3 - 17
server/api/oauth_github_handler.go

@@ -8,6 +8,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/models"
 	"gorm.io/gorm"
 
@@ -17,7 +18,6 @@ import (
 	"golang.org/x/oauth2"
 
 	"github.com/porter-dev/porter/internal/models/integrations"
-	segment "gopkg.in/segmentio/analytics-go.v3"
 )
 
 // HandleGithubOAuthStartUser starts the oauth2 flow for a user login request.
@@ -131,22 +131,8 @@ func (app *App) HandleGithubOAuthCallback(w http.ResponseWriter, r *http.Request
 		}
 
 		// send to segment
-		if app.segmentClient != nil {
-			client := *app.segmentClient
-			client.Enqueue(segment.Identify{
-				UserId: fmt.Sprintf("%v", user.ID),
-				Traits: segment.NewTraits().
-					SetEmail(user.Email).
-					Set("github", "true"),
-			})
-
-			client.Enqueue(segment.Track{
-				UserId: fmt.Sprintf("%v", user.ID),
-				Event:  "New User",
-				Properties: segment.NewProperties().
-					Set("email", user.Email),
-			})
-		}
+		app.analyticsClient.Identify(analytics.CreateSegmentIdentifyNewUser(user, true))
+		app.analyticsClient.Track(analytics.CreateSegmentNewUserTrack(user))
 
 		// log the user in
 		app.Logger.Info().Msgf("New user created: %d", user.ID)

+ 4 - 18
server/api/oauth_google_handler.go

@@ -8,13 +8,12 @@ import (
 	"net/url"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/models"
 	"gorm.io/gorm"
 
 	"github.com/porter-dev/porter/internal/oauth"
 	"golang.org/x/oauth2"
-
-	segment "gopkg.in/segmentio/analytics-go.v3"
 )
 
 // HandleGoogleStartUser starts the oauth2 flow for a user login request.
@@ -96,22 +95,9 @@ func (app *App) HandleGoogleOAuthCallback(w http.ResponseWriter, r *http.Request
 	}
 
 	// send to segment
-	if app.segmentClient != nil {
-		client := *app.segmentClient
-		client.Enqueue(segment.Identify{
-			UserId: fmt.Sprintf("%v", user.ID),
-			Traits: segment.NewTraits().
-				SetEmail(user.Email).
-				Set("github", "true"),
-		})
-
-		client.Enqueue(segment.Track{
-			UserId: fmt.Sprintf("%v", user.ID),
-			Event:  "New User",
-			Properties: segment.NewProperties().
-				Set("email", user.Email),
-		})
-	}
+	app.analyticsClient.Identify(analytics.CreateSegmentIdentifyNewUser(user, true))
+
+	app.analyticsClient.Track(analytics.CreateSegmentNewUserTrack(user))
 
 	// log the user in
 	app.Logger.Info().Msgf("New user created: %d", user.ID)

+ 2 - 10
server/api/release_handler.go

@@ -9,6 +9,7 @@ import (
 	"strings"
 	"sync"
 
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/kubernetes/prometheus"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/templater/parser"
@@ -24,7 +25,6 @@ import (
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/repository"
-	segment "gopkg.in/segmentio/analytics-go.v3"
 	"gopkg.in/yaml.v2"
 )
 
@@ -1087,15 +1087,7 @@ func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	if app.segmentClient != nil {
-		client := *app.segmentClient
-		client.Enqueue(segment.Track{
-			UserId: "anonymous",
-			Event:  "Triggered Re-deploy via Webhook",
-			Properties: segment.NewProperties().
-				Set("repository", repository),
-		})
-	}
+	app.analyticsClient.Track(analytics.CreateSegmentRedeployViaWebhookTrack("anonymous", repository.(string)))
 
 	w.WriteHeader(http.StatusOK)
 }

+ 3 - 18
server/api/user_handler.go

@@ -15,12 +15,12 @@ import (
 	"gorm.io/gorm"
 
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/integrations/email"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
-	segment "gopkg.in/segmentio/analytics-go.v3"
 )
 
 // Enumeration of user API error codes, represented as int64
@@ -51,23 +51,8 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 
 	if err == nil {
 		// send to segment
-		if app.segmentClient != nil {
-			client := *app.segmentClient
-
-			client.Enqueue(segment.Identify{
-				UserId: fmt.Sprintf("%v", user.ID),
-				Traits: segment.NewTraits().
-					SetEmail(user.Email).
-					Set("github", "false"),
-			})
-
-			client.Enqueue(segment.Track{
-				UserId: fmt.Sprintf("%v", user.ID),
-				Event:  "New User",
-				Properties: segment.NewProperties().
-					Set("email", user.Email),
-			})
-		}
+		app.analyticsClient.Identify(analytics.CreateSegmentIdentifyNewUser(user, false))
+		app.analyticsClient.Track(analytics.CreateSegmentNewUserTrack(user))
 
 		app.Logger.Info().Msgf("New user created: %d", user.ID)
 		var redirect string