Ver Fonte

complete analytics onboarding flow

Alexander Belanger há 4 anos atrás
pai
commit
653b525739

+ 14 - 14
cmd/app/main.go

@@ -58,6 +58,19 @@ func main() {
 
 	repo := gorm.NewRepository(db, &key)
 
+	a, err := api.New(&api.AppConfig{
+		Logger:     logger,
+		Repository: repo,
+		ServerConf: appConf.Server,
+		RedisConf:  &appConf.Redis,
+		CapConf:    appConf.Capabilities,
+		DBConf:     appConf.Db,
+	})
+
+	if err != nil {
+		logger.Fatal().Err(err).Msg("")
+	}
+
 	if appConf.Redis.Enabled {
 		redis, err := adapter.NewRedisClient(&appConf.Redis)
 
@@ -70,20 +83,7 @@ func main() {
 
 		errorChan := make(chan error)
 
-		go prov.GlobalStreamListener(redis, *repo, errorChan)
-	}
-
-	a, err := api.New(&api.AppConfig{
-		Logger:     logger,
-		Repository: repo,
-		ServerConf: appConf.Server,
-		RedisConf:  &appConf.Redis,
-		CapConf:    appConf.Capabilities,
-		DBConf:     appConf.Db,
-	})
-
-	if err != nil {
-		logger.Fatal().Err(err).Msg("")
+		go prov.GlobalStreamListener(redis, *repo, a.AnalyticsClient, errorChan)
 	}
 
 	appRouter := router.New(a)

+ 6 - 7
internal/analytics/track_events.go

@@ -15,15 +15,14 @@ const (
 	RegistryProvisioningError   SegmentEvent = "Registry Provisioning Error"
 	RegistryProvisioningSuccess SegmentEvent = "Registry Provisioning Success"
 
-	ClusterConnectedStart   SegmentEvent = "Cluster Connection Started"
-	ClusterConnectedError   SegmentEvent = "Cluster Connection Started"
-	ClusterConnectedSuccess SegmentEvent = "Cluster Connection Error"
+	ClusterConnectionStart   SegmentEvent = "Cluster Connection Started"
+	ClusterConnectionSuccess SegmentEvent = "Cluster Connection Success"
 
-	RegistryConnectedSuccess SegmentEvent = "Registry Connection Success"
-	RegistryConnectedError   SegmentEvent = "Registry Connection Error"
+	RegistryConnectionStart   SegmentEvent = "Registry Connection Started"
+	RegistryConnectionSuccess SegmentEvent = "Registry Connection Success"
 
-	GithubConnectedSuccess SegmentEvent = "Github Connection Success"
-	GithubConnectedError   SegmentEvent = "Github Connection Error"
+	GithubConnectionStart   SegmentEvent = "Github Connection Started"
+	GithubConnectionSuccess SegmentEvent = "Github Connection Success"
 
 	// launch flow
 	ApplicationLaunch            SegmentEvent = "New Application Launched"

+ 199 - 158
internal/analytics/tracks.go

@@ -3,6 +3,7 @@ package analytics
 import (
 	"fmt"
 
+	"github.com/porter-dev/porter/internal/models"
 	segment "gopkg.in/segmentio/analytics-go.v3"
 )
 
@@ -34,14 +35,17 @@ func ProjectCreateTrack(opts *ProjectCreateTrackOpts) segmentTrack {
 }
 
 type ClusterProvisioningStartTrackOpts struct {
+	// note that this is a project-scoped track, since the cluster has not been created yet
 	*ProjectScopedTrackOpts
 
-	ClusterType string // EKS, DOKS, or GKE
+	ClusterType models.InfraKind
+	InfraID     uint
 }
 
 func ClusterProvisioningStartTrack(opts *ClusterProvisioningStartTrackOpts) segmentTrack {
 	additionalProps := make(map[string]interface{})
 	additionalProps["cluster_type"] = opts.ClusterType
+	additionalProps["infra_id"] = opts.InfraID
 
 	return getSegmentProjectTrack(
 		opts.ProjectScopedTrackOpts,
@@ -50,19 +54,20 @@ func ClusterProvisioningStartTrack(opts *ClusterProvisioningStartTrackOpts) segm
 }
 
 type ClusterProvisioningErrorTrackOpts struct {
-	*ClusterScopedTrackOpts
+	// note that this is a project-scoped track, since the cluster has not been created yet
+	*ProjectScopedTrackOpts
 
-	ClusterType string // EKS, DOKS, or GKE
-	Errors      []string
+	ClusterType models.InfraKind
+	InfraID     uint
 }
 
 func ClusterProvisioningErrorTrack(opts *ClusterProvisioningErrorTrackOpts) segmentTrack {
 	additionalProps := make(map[string]interface{})
 	additionalProps["cluster_type"] = opts.ClusterType
-	additionalProps["errors"] = opts.Errors
+	additionalProps["infra_id"] = opts.InfraID
 
-	return getSegmentClusterTrack(
-		opts.ClusterScopedTrackOpts,
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
 		getDefaultSegmentTrack(additionalProps, ClusterProvisioningError),
 	)
 }
@@ -70,14 +75,14 @@ func ClusterProvisioningErrorTrack(opts *ClusterProvisioningErrorTrackOpts) segm
 type ClusterProvisioningSuccessTrackOpts struct {
 	*ClusterScopedTrackOpts
 
-	ClusterType   string // EKS, DOKS, or GKE
-	ClusterServer string
+	ClusterType models.InfraKind
+	InfraID     uint
 }
 
 func ClusterProvisioningSuccessTrack(opts *ClusterProvisioningSuccessTrackOpts) segmentTrack {
 	additionalProps := make(map[string]interface{})
 	additionalProps["cluster_type"] = opts.ClusterType
-	additionalProps["cluster_server"] = opts.ClusterServer
+	additionalProps["infra_id"] = opts.InfraID
 
 	return getSegmentClusterTrack(
 		opts.ClusterScopedTrackOpts,
@@ -85,6 +90,102 @@ func ClusterProvisioningSuccessTrack(opts *ClusterProvisioningSuccessTrackOpts)
 	)
 }
 
+type ClusterConnectionStartTrackOpts struct {
+	// note that this is a project-scoped track, since the cluster has not been created yet
+	*ProjectScopedTrackOpts
+
+	ClusterCandidateID uint
+}
+
+func ClusterConnectionStartTrack(opts *ClusterConnectionStartTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["cc_id"] = opts.ClusterCandidateID
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, ClusterConnectionStart),
+	)
+}
+
+type ClusterConnectionSuccessTrackOpts struct {
+	*ClusterScopedTrackOpts
+
+	ClusterCandidateID uint
+}
+
+func ClusterConnectionSuccessTrack(opts *ClusterConnectionSuccessTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["cc_id"] = opts.ClusterCandidateID
+
+	return getSegmentClusterTrack(
+		opts.ClusterScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, ClusterConnectionSuccess),
+	)
+}
+
+type RegistryConnectionStartTrackOpts struct {
+	// note that this is a project-scoped track, since the cluster has not been created yet
+	*ProjectScopedTrackOpts
+
+	// a random id assigned to this connection request
+	FlowID string
+}
+
+func RegistryConnectionStartTrack(opts *RegistryConnectionStartTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["flow_id"] = opts.FlowID
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, RegistryConnectionStart),
+	)
+}
+
+type RegistryConnectionSuccessTrackOpts struct {
+	*RegistryScopedTrackOpts
+
+	// a random id assigned to this connection request
+	FlowID string
+}
+
+func RegistryConnectionSuccessTrack(opts *RegistryConnectionSuccessTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["flow_id"] = opts.FlowID
+
+	return getSegmentRegistryTrack(
+		opts.RegistryScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, RegistryConnectionSuccess),
+	)
+}
+
+type GithubConnectionStartTrackOpts struct {
+	// note that this is a user-scoped track, since github repos are tied to the user
+	*UserScopedTrackOpts
+}
+
+func GithubConnectionStartTrack(opts *GithubConnectionStartTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+
+	return getSegmentUserTrack(
+		opts.UserScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, GithubConnectionStart),
+	)
+}
+
+type GithubConnectionSuccessTrackOpts struct {
+	// note that this is a user-scoped track, since github repos are tied to the user
+	*UserScopedTrackOpts
+}
+
+func GithubConnectionSuccessTrack(opts *GithubConnectionSuccessTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+
+	return getSegmentUserTrack(
+		opts.UserScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, GithubConnectionSuccess),
+	)
+}
+
 type ApplicationDeploymentWebhookTrackOpts struct {
 	*ApplicationScopedTrackOpts
 
@@ -101,6 +202,62 @@ func ApplicationDeploymentWebhookTrack(opts *ApplicationDeploymentWebhookTrackOp
 	)
 }
 
+type RegistryProvisioningStartTrackOpts struct {
+	// note that this is a project-scoped track, since the registry has not been created yet
+	*ProjectScopedTrackOpts
+
+	RegistryType models.InfraKind
+	InfraID      uint
+}
+
+func RegistryProvisioningStartTrack(opts *RegistryProvisioningStartTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["registry_type"] = opts.RegistryType
+	additionalProps["infra_id"] = opts.InfraID
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, RegistryProvisioningStart),
+	)
+}
+
+type RegistryProvisioningErrorTrackOpts struct {
+	// note that this is a project-scoped track, since the registry has not been created yet
+	*ProjectScopedTrackOpts
+
+	RegistryType models.InfraKind
+	InfraID      uint
+}
+
+func RegistryProvisioningErrorTrack(opts *RegistryProvisioningErrorTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["registry_type"] = opts.RegistryType
+	additionalProps["infra_id"] = opts.InfraID
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, RegistryProvisioningError),
+	)
+}
+
+type RegistryProvisioningSuccessTrackOpts struct {
+	*RegistryScopedTrackOpts
+
+	RegistryType models.InfraKind
+	InfraID      uint
+}
+
+func RegistryProvisioningSuccessTrack(opts *RegistryProvisioningSuccessTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["registry_type"] = opts.RegistryType
+	additionalProps["infra_id"] = opts.InfraID
+
+	return getSegmentRegistryTrack(
+		opts.RegistryScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, RegistryProvisioningSuccess),
+	)
+}
+
 // HELPERS
 
 type segmentTrack interface {
@@ -158,6 +315,10 @@ func (p segmentProperties) addClusterProperties(opts *ClusterScopedTrackOpts) {
 	p["cluster_id"] = opts.ClusterID
 }
 
+func (p segmentProperties) addRegistryProperties(opts *RegistryScopedTrackOpts) {
+	p["registry_id"] = opts.RegistryID
+}
+
 func (p segmentProperties) addApplicationProperties(opts *ApplicationScopedTrackOpts) {
 	p["app_name"] = opts.Name
 	p["app_namespace"] = opts.Namespace
@@ -226,6 +387,34 @@ func getSegmentProjectTrack(opts *ProjectScopedTrackOpts, track *defaultSegmentT
 	}
 }
 
+type RegistryScopedTrack struct {
+	*ProjectScopedTrack
+
+	registryID uint
+}
+
+type RegistryScopedTrackOpts struct {
+	*ProjectScopedTrackOpts
+
+	RegistryID uint
+}
+
+func GetRegistryScopedTrackOpts(userID, projID, regID uint) *RegistryScopedTrackOpts {
+	return &RegistryScopedTrackOpts{
+		ProjectScopedTrackOpts: GetProjectScopedTrackOpts(userID, projID),
+		RegistryID:             regID,
+	}
+}
+
+func getSegmentRegistryTrack(opts *RegistryScopedTrackOpts, track *defaultSegmentTrack) *RegistryScopedTrack {
+	track.properties.addRegistryProperties(opts)
+
+	return &RegistryScopedTrack{
+		ProjectScopedTrack: getSegmentProjectTrack(opts.ProjectScopedTrackOpts, track),
+		registryID:         opts.RegistryID,
+	}
+}
+
 type ClusterScopedTrack struct {
 	*ProjectScopedTrack
 
@@ -285,151 +474,3 @@ func getSegmentApplicationTrack(opts *ApplicationScopedTrackOpts, track *default
 		namespace:          opts.Namespace,
 	}
 }
-
-// type segmentProjectScopedTrack struct {
-// 	*UserScopedTrack
-
-// 	ProjID uint
-// 	ProjName string
-// }
-
-// type segmentClusterScopedTrack struct {
-// 	*segmentProjectScopedTrack
-
-// 	clusterID uint
-// 	clusterName string
-// }
-
-// type segmentApplicationScopedTrack struct {
-// 	*segmentClusterScopedTrack
-
-// 	appName string
-// 	appNamespace string
-// }
-
-// // CreateSegmentNewUserTrack creates a track of type "New User", which
-// // 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
-// }
-
-// // CreateSegmentRedeployViaWebhookTrack creates a track of type "Triggered Re-deploy via Webhook", which
-// // 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)
-// }
-
-// type segmentNewProjectEventTrack struct {
-// 	userId   string
-// 	projId   string
-// 	projName string
-// }
-
-// // NewProjectEventOpts are the parameters for creating a "New Project Event" track
-// type NewProjectEventOpts struct {
-// 	UserID   string
-// 	ProjID   string
-// 	ProjName string
-// }
-
-// // CreateSegmentNewProjectEvent creates a track of type "New Project Event", which
-// // tracks whenever a cluster is newly provisioned, connected, or destroyed.
-// func CreateSegmentNewProjectEvent(opts *NewProjectEventOpts) *segmentNewProjectEventTrack {
-// 	return &segmentNewProjectEventTrack{
-// 		userId:   opts.UserID,
-// 		projId:   opts.ProjID,
-// 		projName: opts.ProjName,
-// 	}
-// }
-
-// func (t *segmentNewProjectEventTrack) getUserId() string {
-// 	return t.userId
-// }
-
-// func (t *segmentNewProjectEventTrack) getEvent() SegmentEvent {
-// 	return NewProjectEvent
-// }
-
-// func (t *segmentNewProjectEventTrack) getProperties() segment.Properties {
-// 	return segment.NewProperties().
-// 		Set("Project ID", t.projId).
-// 		Set("Project Name", t.projName)
-// }
-
-// type segmentNewClusterEventTrack struct {
-// 	userId      string
-// 	projId      string
-// 	clusterName string
-// 	clusterType string // EKS, DOKS, or GKE
-// 	eventType   string // connected, provisioned, or destroyed
-// }
-
-// // NewClusterEventOpts are the parameters for creating a "New Cluster Event" track
-// type NewClusterEventOpts struct {
-// 	UserId      string
-// 	ProjId      string
-// 	ClusterName string
-// 	ClusterType string // EKS, DOKS, or GKE
-// 	EventType   string // connected, provisioned, or destroyed
-// }
-
-// // CreateSegmentNewClusterEvent creates a track of type "New Cluster Event", which
-// // tracks whenever a cluster is newly provisioned, connected, or destroyed.
-// func CreateSegmentNewClusterEvent(opts *NewClusterEventOpts) *segmentNewClusterEventTrack {
-// 	return &segmentNewClusterEventTrack{
-// 		userId:      opts.UserId,
-// 		projId:      opts.ProjId,
-// 		clusterName: opts.ClusterName,
-// 		clusterType: opts.ClusterType,
-// 		eventType:   opts.EventType,
-// 	}
-// }
-
-// func (t *segmentNewClusterEventTrack) getUserId() string {
-// 	return t.userId
-// }
-
-// func (t *segmentNewClusterEventTrack) getEvent() SegmentEvent {
-// 	return NewClusterEvent
-// }
-
-// func (t *segmentNewClusterEventTrack) getProperties() segment.Properties {
-// 	return segment.NewProperties().Set("Project ID", t.projId).Set("Cluster Name", t.clusterName).Set("Cluster Type", t.clusterType).Set("Event Type", t.eventType)
-// }

+ 68 - 0
internal/kubernetes/provisioner/global_stream.go

@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"github.com/aws/aws-sdk-go/service/ecr"
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/repository"
 
 	redis "github.com/go-redis/redis/v8"
@@ -83,6 +84,7 @@ type ResourceCRUDHandler interface {
 func GlobalStreamListener(
 	client *redis.Client,
 	repo repository.Repository,
+	analyticsClient analytics.AnalyticsSegmentClient,
 	errorChan chan error,
 ) {
 	for {
@@ -163,6 +165,14 @@ func GlobalStreamListener(
 					if err != nil {
 						continue
 					}
+
+					analyticsClient.Track(analytics.RegistryProvisioningSuccessTrack(
+						&analytics.RegistryProvisioningSuccessTrackOpts{
+							RegistryScopedTrackOpts: analytics.GetRegistryScopedTrackOpts(infra.CreatedByUserID, infra.ProjectID, reg.ID),
+							RegistryType:            infra.Kind,
+							InfraID:                 infra.ID,
+						},
+					))
 				} else if kind == string(models.InfraEKS) {
 					cluster := &models.Cluster{
 						AuthMechanism:    models.AWS,
@@ -197,6 +207,14 @@ func GlobalStreamListener(
 					if err != nil {
 						continue
 					}
+
+					analyticsClient.Track(analytics.ClusterProvisioningSuccessTrack(
+						&analytics.ClusterProvisioningSuccessTrackOpts{
+							ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(infra.CreatedByUserID, infra.ProjectID, cluster.ID),
+							ClusterType:            infra.Kind,
+							InfraID:                infra.ID,
+						},
+					))
 				} else if kind == string(models.InfraGCR) {
 					reg := &models.Registry{
 						ProjectID:        projID,
@@ -217,6 +235,14 @@ func GlobalStreamListener(
 					if err != nil {
 						continue
 					}
+
+					analyticsClient.Track(analytics.RegistryProvisioningSuccessTrack(
+						&analytics.RegistryProvisioningSuccessTrackOpts{
+							RegistryScopedTrackOpts: analytics.GetRegistryScopedTrackOpts(infra.CreatedByUserID, infra.ProjectID, reg.ID),
+							RegistryType:            infra.Kind,
+							InfraID:                 infra.ID,
+						},
+					))
 				} else if kind == string(models.InfraGKE) {
 					cluster := &models.Cluster{
 						AuthMechanism:    models.GCP,
@@ -251,6 +277,14 @@ func GlobalStreamListener(
 					if err != nil {
 						continue
 					}
+
+					analyticsClient.Track(analytics.ClusterProvisioningSuccessTrack(
+						&analytics.ClusterProvisioningSuccessTrackOpts{
+							ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(infra.CreatedByUserID, infra.ProjectID, cluster.ID),
+							ClusterType:            infra.Kind,
+							InfraID:                infra.ID,
+						},
+					))
 				} else if kind == string(models.InfraDOCR) {
 					reg := &models.Registry{
 						ProjectID:       projID,
@@ -270,6 +304,14 @@ func GlobalStreamListener(
 					if err != nil {
 						continue
 					}
+
+					analyticsClient.Track(analytics.RegistryProvisioningSuccessTrack(
+						&analytics.RegistryProvisioningSuccessTrackOpts{
+							RegistryScopedTrackOpts: analytics.GetRegistryScopedTrackOpts(infra.CreatedByUserID, infra.ProjectID, reg.ID),
+							RegistryType:            infra.Kind,
+							InfraID:                 infra.ID,
+						},
+					))
 				} else if kind == string(models.InfraDOKS) {
 					cluster := &models.Cluster{
 						AuthMechanism:   models.DO,
@@ -304,6 +346,14 @@ func GlobalStreamListener(
 					if err != nil {
 						continue
 					}
+
+					analyticsClient.Track(analytics.ClusterProvisioningSuccessTrack(
+						&analytics.ClusterProvisioningSuccessTrackOpts{
+							ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(infra.CreatedByUserID, infra.ProjectID, cluster.ID),
+							ClusterType:            infra.Kind,
+							InfraID:                infra.ID,
+						},
+					))
 				}
 			} else if fmt.Sprintf("%v", msg.Values["status"]) == "error" {
 				infra, err := repo.Infra.ReadInfra(infraID)
@@ -319,6 +369,24 @@ func GlobalStreamListener(
 				if err != nil {
 					continue
 				}
+
+				if infra.Kind == models.InfraDOKS || infra.Kind == models.InfraGKE || infra.Kind == models.InfraEKS {
+					analyticsClient.Track(analytics.ClusterProvisioningErrorTrack(
+						&analytics.ClusterProvisioningErrorTrackOpts{
+							ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(infra.CreatedByUserID, infra.ProjectID),
+							ClusterType:            infra.Kind,
+							InfraID:                infra.ID,
+						},
+					))
+				} else if infra.Kind == models.InfraDOCR || infra.Kind == models.InfraGCR || infra.Kind == models.InfraECR {
+					analyticsClient.Track(analytics.RegistryProvisioningErrorTrack(
+						&analytics.RegistryProvisioningErrorTrackOpts{
+							ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(infra.CreatedByUserID, infra.ProjectID),
+							RegistryType:           infra.Kind,
+							InfraID:                infra.ID,
+						},
+					))
+				}
 			} else if fmt.Sprintf("%v", msg.Values["status"]) == "destroyed" {
 				infra, err := repo.Infra.ReadInfra(infraID)
 

+ 3 - 0
internal/models/infra.go

@@ -48,6 +48,9 @@ type Infra struct {
 	// The project that this infra belongs to
 	ProjectID uint `json:"project_id"`
 
+	// The ID of the user that created this infra
+	CreatedByUserID uint
+
 	// Status is the status of the infra
 	Status InfraStatus `json:"status"`
 

+ 8 - 6
server/api/api.go

@@ -95,11 +95,13 @@ type App struct {
 	GoogleUserConf    *oauth2.Config
 	SlackConf         *oauth2.Config
 
-	db              *gorm.DB
-	validator       *vr.Validate
-	translator      *ut.Translator
-	tokenConf       *token.TokenGeneratorConf
-	analyticsClient analytics.AnalyticsSegmentClient
+	// analytics client for reporting
+	AnalyticsClient analytics.AnalyticsSegmentClient
+
+	db         *gorm.DB
+	validator  *vr.Validate
+	translator *ut.Translator
+	tokenConf  *token.TokenGeneratorConf
 }
 
 type AppCapabilities struct {
@@ -242,7 +244,7 @@ func New(conf *AppConfig) (*App, error) {
 	}
 
 	newSegmentClient := analytics.InitializeAnalyticsSegmentClient(sc.SegmentClientKey, app.Logger)
-	app.analyticsClient = newSegmentClient
+	app.AnalyticsClient = newSegmentClient
 
 	app.updateChartRepoURLs()
 

+ 22 - 0
server/api/cluster_handler.go

@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/domain"
@@ -286,6 +287,13 @@ func (app *App) HandleCreateProjectClusterCandidates(w http.ResponseWriter, r *h
 			return
 		}
 
+		app.AnalyticsClient.Track(analytics.ClusterConnectionStartTrack(
+			&analytics.ClusterConnectionStartTrackOpts{
+				ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(userID, uint(projID)),
+				ClusterCandidateID:     cc.ID,
+			},
+		))
+
 		app.Logger.Info().Msgf("New cluster candidate created: %d", cc.ID)
 
 		// if the ClusterCandidate does not have any actions to perform, create the Cluster
@@ -328,6 +336,13 @@ func (app *App) HandleCreateProjectClusterCandidates(w http.ResponseWriter, r *h
 			}
 
 			app.Logger.Info().Msgf("New cluster created: %d", cluster.ID)
+
+			app.AnalyticsClient.Track(analytics.ClusterConnectionSuccessTrack(
+				&analytics.ClusterConnectionSuccessTrackOpts{
+					ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(userID, uint(projID), cluster.ID),
+					ClusterCandidateID:     cc.ID,
+				},
+			))
 		}
 
 		extClusters = append(extClusters, cc.Externalize())
@@ -437,6 +452,13 @@ func (app *App) HandleResolveClusterCandidate(w http.ResponseWriter, r *http.Req
 
 	app.Logger.Info().Msgf("New cluster created: %d", cluster.ID)
 
+	app.AnalyticsClient.Track(analytics.ClusterConnectionSuccessTrack(
+		&analytics.ClusterConnectionSuccessTrackOpts{
+			ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(userID, uint(projID), cluster.ID),
+			ClusterCandidateID:     uint(candID),
+		},
+	))
+
 	clusterExt := cluster.Externalize()
 
 	w.WriteHeader(http.StatusCreated)

+ 9 - 0
server/api/integration_handler.go

@@ -16,6 +16,7 @@ import (
 
 	"github.com/go-chi/chi"
 	"github.com/google/go-github/github"
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/oauth"
 	"golang.org/x/oauth2"
@@ -481,6 +482,14 @@ func (app *App) HandleGithubAppAuthorize(w http.ResponseWriter, r *http.Request)
 
 // HandleGithubAppOauthInit redirects the user to the Porter github app authorization page
 func (app *App) HandleGithubAppOauthInit(w http.ResponseWriter, r *http.Request) {
+	userID, _ := app.getUserIDFromRequest(r)
+
+	app.AnalyticsClient.Track(analytics.GithubConnectionStartTrack(
+		&analytics.GithubConnectionStartTrackOpts{
+			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(userID),
+		},
+	))
+
 	http.Redirect(w, r, app.GithubAppConf.AuthCodeURL("", oauth2.AccessTypeOffline), 302)
 }
 

+ 8 - 2
server/api/oauth_github_handler.go

@@ -131,9 +131,9 @@ func (app *App) HandleGithubOAuthCallback(w http.ResponseWriter, r *http.Request
 		}
 
 		// send to segment
-		app.analyticsClient.Identify(analytics.CreateSegmentIdentifyUser(user))
+		app.AnalyticsClient.Identify(analytics.CreateSegmentIdentifyUser(user))
 
-		app.analyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
+		app.AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
 			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
 		}))
 
@@ -355,6 +355,12 @@ func (app *App) HandleGithubAppOAuthCallback(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	app.AnalyticsClient.Track(analytics.GithubConnectionSuccessTrack(
+		&analytics.GithubConnectionSuccessTrackOpts{
+			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
+		},
+	))
+
 	if session.Values["query_params"] != "" {
 		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
 	} else {

+ 2 - 2
server/api/oauth_google_handler.go

@@ -95,9 +95,9 @@ func (app *App) HandleGoogleOAuthCallback(w http.ResponseWriter, r *http.Request
 	}
 
 	// send to segment
-	app.analyticsClient.Identify(analytics.CreateSegmentIdentifyUser(user))
+	app.AnalyticsClient.Identify(analytics.CreateSegmentIdentifyUser(user))
 
-	app.analyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
+	app.AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
 		UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
 	}))
 

+ 5 - 0
server/api/project_handler.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -72,6 +73,10 @@ func (app *App) HandleCreateProject(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	app.AnalyticsClient.Track(analytics.ProjectCreateTrack(&analytics.ProjectCreateTrackOpts{
+		ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(userID, projModel.ID),
+	}))
+
 	app.Logger.Info().Msgf("New project created: %d", projModel.ID)
 
 	w.WriteHeader(http.StatusCreated)

+ 51 - 6
server/api/provision_handler.go

@@ -20,6 +20,7 @@ import (
 // container pod
 func (app *App) HandleProvisionTestInfra(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+	userID, err := app.getUserIDFromRequest(r)
 
 	if err != nil || projID == 0 {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
@@ -44,6 +45,8 @@ func (app *App) HandleProvisionTestInfra(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	infra.CreatedByUserID = userID
+
 	// handle write to the database
 	infra, err = app.Repo.Infra.CreateInfra(infra)
 
@@ -145,6 +148,7 @@ func (app *App) HandleDestroyTestInfra(w http.ResponseWriter, r *http.Request) {
 // HandleProvisionAWSECRInfra provisions a new aws ECR instance for a project
 func (app *App) HandleProvisionAWSECRInfra(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+	userID, err := app.getUserIDFromRequest(r)
 
 	if err != nil || projID == 0 {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
@@ -175,6 +179,8 @@ func (app *App) HandleProvisionAWSECRInfra(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
+	infra.CreatedByUserID = userID
+
 	// handle write to the database
 	infra, err = app.Repo.Infra.CreateInfra(infra)
 
@@ -217,6 +223,14 @@ func (app *App) HandleProvisionAWSECRInfra(w http.ResponseWriter, r *http.Reques
 
 	app.Logger.Info().Msgf("New aws ecr infra created: %d", infra.ID)
 
+	app.AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
+		&analytics.RegistryProvisioningStartTrackOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(userID, uint(projID)),
+			RegistryType:           models.InfraECR,
+			InfraID:                infra.ID,
+		},
+	))
+
 	w.WriteHeader(http.StatusCreated)
 
 	infraExt := infra.Externalize()
@@ -334,6 +348,8 @@ func (app *App) HandleProvisionAWSEKSInfra(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
+	infra.CreatedByUserID = userID
+
 	// handle write to the database
 	infra, err = app.Repo.Infra.CreateInfra(infra)
 
@@ -377,10 +393,11 @@ func (app *App) HandleProvisionAWSEKSInfra(w http.ResponseWriter, r *http.Reques
 
 	app.Logger.Info().Msgf("New aws eks infra created: %d", infra.ID)
 
-	app.analyticsClient.Track(analytics.ClusterProvisioningStartTrack(
+	app.AnalyticsClient.Track(analytics.ClusterProvisioningStartTrack(
 		&analytics.ClusterProvisioningStartTrackOpts{
 			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(userID, uint(projID)),
-			ClusterType:            "EKS",
+			ClusterType:            models.InfraEKS,
+			InfraID:                infra.ID,
 		},
 	))
 
@@ -472,6 +489,7 @@ func (app *App) HandleDestroyAWSEKSInfra(w http.ResponseWriter, r *http.Request)
 // HandleProvisionGCPGCRInfra enables GCR for a project
 func (app *App) HandleProvisionGCPGCRInfra(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+	userID, err := app.getUserIDFromRequest(r)
 
 	if err != nil || projID == 0 {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
@@ -502,6 +520,8 @@ func (app *App) HandleProvisionGCPGCRInfra(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
+	infra.CreatedByUserID = userID
+
 	// handle write to the database
 	infra, err = app.Repo.Infra.CreateInfra(infra)
 
@@ -543,6 +563,14 @@ func (app *App) HandleProvisionGCPGCRInfra(w http.ResponseWriter, r *http.Reques
 
 	app.Logger.Info().Msgf("New gcp gcr infra created: %d", infra.ID)
 
+	app.AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
+		&analytics.RegistryProvisioningStartTrackOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(userID, uint(projID)),
+			RegistryType:           models.InfraGCR,
+			InfraID:                infra.ID,
+		},
+	))
+
 	w.WriteHeader(http.StatusCreated)
 
 	infraExt := infra.Externalize()
@@ -587,6 +615,8 @@ func (app *App) HandleProvisionGCPGKEInfra(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
+	infra.CreatedByUserID = userID
+
 	// handle write to the database
 	infra, err = app.Repo.Infra.CreateInfra(infra)
 
@@ -629,10 +659,11 @@ func (app *App) HandleProvisionGCPGKEInfra(w http.ResponseWriter, r *http.Reques
 
 	app.Logger.Info().Msgf("New gcp gke infra created: %d", infra.ID)
 
-	app.analyticsClient.Track(analytics.ClusterProvisioningStartTrack(
+	app.AnalyticsClient.Track(analytics.ClusterProvisioningStartTrack(
 		&analytics.ClusterProvisioningStartTrackOpts{
 			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(userID, uint(projID)),
-			ClusterType:            "GKE",
+			ClusterType:            models.InfraGKE,
+			InfraID:                infra.ID,
 		},
 	))
 
@@ -767,6 +798,7 @@ func (app *App) HandleGetProvisioningLogs(w http.ResponseWriter, r *http.Request
 // HandleProvisionDODOCRInfra provisions a new digitalocean DOCR instance for a project
 func (app *App) HandleProvisionDODOCRInfra(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+	userID, err := app.getUserIDFromRequest(r)
 
 	if err != nil || projID == 0 {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
@@ -797,6 +829,8 @@ func (app *App) HandleProvisionDODOCRInfra(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
+	infra.CreatedByUserID = userID
+
 	// handle write to the database
 	infra, err = app.Repo.Infra.CreateInfra(infra)
 
@@ -841,6 +875,14 @@ func (app *App) HandleProvisionDODOCRInfra(w http.ResponseWriter, r *http.Reques
 
 	app.Logger.Info().Msgf("New do docr infra created: %d", infra.ID)
 
+	app.AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
+		&analytics.RegistryProvisioningStartTrackOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(userID, uint(projID)),
+			RegistryType:           models.InfraDOCR,
+			InfraID:                infra.ID,
+		},
+	))
+
 	w.WriteHeader(http.StatusCreated)
 
 	infraExt := infra.Externalize()
@@ -960,6 +1002,8 @@ func (app *App) HandleProvisionDODOKSInfra(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
+	infra.CreatedByUserID = userID
+
 	// handle write to the database
 	infra, err = app.Repo.Infra.CreateInfra(infra)
 
@@ -1004,10 +1048,11 @@ func (app *App) HandleProvisionDODOKSInfra(w http.ResponseWriter, r *http.Reques
 
 	app.Logger.Info().Msgf("New do doks infra created: %d", infra.ID)
 
-	app.analyticsClient.Track(analytics.ClusterProvisioningStartTrack(
+	app.AnalyticsClient.Track(analytics.ClusterProvisioningStartTrack(
 		&analytics.ClusterProvisioningStartTrackOpts{
 			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(userID, uint(projID)),
-			ClusterType:            "DOKS",
+			ClusterType:            models.InfraDOKS,
+			InfraID:                infra.ID,
 		},
 	))
 

+ 17 - 1
server/api/registry_handler.go

@@ -8,8 +8,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/oauth"
-
 	"github.com/porter-dev/porter/internal/registry"
 
 	"github.com/go-chi/chi"
@@ -22,12 +22,21 @@ import (
 // HandleCreateRegistry creates a new registry
 func (app *App) HandleCreateRegistry(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+	userID, err := app.getUserIDFromRequest(r)
+	flowID := oauth.CreateRandomState()
 
 	if err != nil || projID == 0 {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 	}
 
+	app.AnalyticsClient.Track(analytics.RegistryConnectionStartTrack(
+		&analytics.RegistryConnectionStartTrackOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(userID, uint(projID)),
+			FlowID:                 flowID,
+		},
+	))
+
 	form := &forms.CreateRegistry{
 		ProjectID: uint(projID),
 	}
@@ -62,6 +71,13 @@ func (app *App) HandleCreateRegistry(w http.ResponseWriter, r *http.Request) {
 
 	app.Logger.Info().Msgf("New registry created: %d", registry.ID)
 
+	app.AnalyticsClient.Track(analytics.RegistryConnectionSuccessTrack(
+		&analytics.RegistryConnectionSuccessTrackOpts{
+			RegistryScopedTrackOpts: analytics.GetRegistryScopedTrackOpts(userID, uint(projID), registry.ID),
+			FlowID:                  flowID,
+		},
+	))
+
 	w.WriteHeader(http.StatusCreated)
 
 	regExt := registry.Externalize()

+ 1 - 1
server/api/release_handler.go

@@ -1317,7 +1317,7 @@ func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Reques
 
 	userID, _ := app.getUserIDFromRequest(r)
 
-	app.analyticsClient.Track(analytics.ApplicationDeploymentWebhookTrack(&analytics.ApplicationDeploymentWebhookTrackOpts{
+	app.AnalyticsClient.Track(analytics.ApplicationDeploymentWebhookTrack(&analytics.ApplicationDeploymentWebhookTrackOpts{
 		ImageURI: fmt.Sprintf("%v", repository),
 		ApplicationScopedTrackOpts: analytics.GetApplicationScopedTrackOpts(
 			userID,

+ 2 - 2
server/api/user_handler.go

@@ -54,9 +54,9 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 
 	if err == nil {
 		// send to segment
-		app.analyticsClient.Identify(analytics.CreateSegmentIdentifyUser(user))
+		app.AnalyticsClient.Identify(analytics.CreateSegmentIdentifyUser(user))
 
-		app.analyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
+		app.AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
 			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
 		}))