Просмотр исходного кода

Merge pull request #2771 from porter-dev/ss-analytics

serverside analytics
jusrhee 3 лет назад
Родитель
Сommit
95d2512606

+ 3 - 0
api/server/handlers/user/create.go

@@ -108,6 +108,9 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	u.Config().AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
 		UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
 		Email:               user.Email,
+		FirstName:           user.FirstName,
+		LastName:            user.LastName,
+		CompanyName:         user.CompanyName,
 	}))
 
 	if redirect != "" {

+ 0 - 5
api/server/handlers/user/github_callback.go

@@ -163,11 +163,6 @@ func upsertUserFromToken(config *config.Config, tok *oauth2.Token) (*models.User
 			if err != nil {
 				return nil, err
 			}
-
-			config.AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
-				UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
-				Email:               user.Email,
-			}))
 		} else if err == nil {
 			return nil, fmt.Errorf("email already registered")
 		} else if err != nil {

+ 0 - 5
api/server/handlers/user/google_callback.go

@@ -148,11 +148,6 @@ func upsertGoogleUserFromToken(config *config.Config, tok *oauth2.Token) (*model
 			if err != nil {
 				return nil, err
 			}
-
-			config.AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
-				UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
-				Email:               user.Email,
-			}))
 		} else if err == nil {
 			return nil, fmt.Errorf("email already registered")
 		} else if err != nil {

+ 55 - 0
api/server/handlers/user/update_onboarding_step.go

@@ -0,0 +1,55 @@
+package user
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/analytics"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type UpdateOnboardingStepHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewUpdateOnboardingStepHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpdateOnboardingStepHandler {
+	return &UpdateOnboardingStepHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (v *UpdateOnboardingStepHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+	request := &types.UpdateOnboardingStepRequest{}
+	if ok := v.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	if request.Step == "cost-consent-complete" {
+		v.Config().AnalyticsClient.Track(analytics.CostConsentTrack(&analytics.CostConsentTrackOpts{
+			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
+		}))
+	}
+
+	if request.Step == "credential-step-complete" {
+		v.Config().AnalyticsClient.Track(analytics.CredentialStepTrack(&analytics.CredentialStepTrackOpts{
+			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
+		}))
+	}
+
+	if request.Step == "provisioning-started" {
+		v.Config().AnalyticsClient.Track(analytics.ProvisioningAttemptTrack(&analytics.ProvisioningAttemptTrackOpts{
+			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
+		}))
+	}
+
+	v.WriteResult(w, r, user.ToUserType())
+}

+ 9 - 0
api/server/handlers/user/update_user_info.go

@@ -8,6 +8,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/models"
 )
 
@@ -39,6 +40,14 @@ func (v *UpdateUserInfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		user.CompanyName = request.CompanyName
 	}
 
+	v.Config().AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
+		UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
+		Email:               user.Email,
+		FirstName:           user.FirstName,
+		LastName:            user.LastName,
+		CompanyName:         user.CompanyName,
+	}))
+
 	user, err := v.Repo().User().UpdateUser(user)
 	if err != nil {
 		v.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 25 - 0
api/server/router/user.go

@@ -146,6 +146,31 @@ func getUserRoutes(
 		Router:   r,
 	})
 
+	// POST /api/onboarding_step -> user.UpdateOnboardingStepHandler
+	updateOnboardingStepEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/onboarding_step",
+			},
+			Scopes: []types.PermissionScope{types.UserScope},
+		},
+	)
+
+	updateOnboardingStepHandler := user.NewUpdateOnboardingStepHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: updateOnboardingStepEndpoint,
+		Handler:  updateOnboardingStepHandler,
+		Router:   r,
+	})
+
 	// GET /api/users/current -> user.NewUserGetCurrentHandler
 	authCheckEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 4 - 0
api/types/user.go

@@ -80,3 +80,7 @@ type UpdateUserInfoRequest struct {
 	LastName    string `json:"last_name" form:"required,max=255"`
 	CompanyName string `json:"company_name" form:"required,max=255"`
 }
+
+type UpdateOnboardingStepRequest struct {
+	Step string `json:"step" form:"required,max=255"`
+}

+ 14 - 0
dashboard/src/components/ProvisionerFlow.tsx

@@ -3,6 +3,7 @@ import styled from "styled-components";
 
 import { integrationList } from "shared/common";
 import { Context } from "shared/Context";
+import api from "shared/api";
 
 import ProvisionerForm from "components/ProvisionerForm";
 import CredentialsForm from "components/CredentialsForm";
@@ -36,6 +37,18 @@ const ProvisionerFlow: React.FC<Props> = ({
     return usage?.current.clusters >= usage?.limit.clusters;
   }, [usage]);
 
+  const markStepCostConsent = async () => {
+    try {
+      const res = await api.updateOnboardingStep(
+        "<token>", 
+        { step: "cost-consent-complete" }, 
+        {}
+      );
+    } catch (err) {
+      console.log(err);
+    }
+  }
+
   if (currentStep === "cloud") {
     return (
       <>
@@ -121,6 +134,7 @@ const ProvisionerFlow: React.FC<Props> = ({
               onClick={() => {
                 setShowCostConfirmModal(false);
                 setConfirmCost("");
+                markStepCostConsent();
                 setCurrentStep("credentials");
               }}
             >

+ 1 - 1
dashboard/src/components/ProvisionerForm.tsx

@@ -28,7 +28,7 @@ const ProvisionerForm: React.FC<Props> = ({
         Configure settings
       </Heading>
       <Helper>
-        Configure settings for your new cluster. 
+        Configure settings for your new cluster.
       </Helper>
       <ProvisionerSettings credentialId={credentialId} />
     </>

+ 14 - 0
dashboard/src/components/ProvisionerSettings.tsx

@@ -76,7 +76,21 @@ const ProvisionerSettings: React.FC<Props> = props => {
   const [clusterVersion, setClusterVersion] = useState("v1.24.0");
   const [isReadOnly, setIsReadOnly] = useState(false);
 
+  const markProvisioningStarted = async () => {
+    try {
+      const res = await api.updateOnboardingStep(
+        "<token>", 
+        { step: "provisioning-started" }, 
+        {}
+      );
+    } catch (err) {
+      console.log(err);
+    }
+  }
+
   const createCluster = async () => {
+    markProvisioningStarted();
+    
     var data = new Contract({
       cluster: new Cluster({
         projectId: currentProject.id,

+ 13 - 0
dashboard/src/shared/api.tsx

@@ -2236,6 +2236,17 @@ const getIncidentEvents = baseApi<
     `/api/projects/${project_id}/clusters/${cluster_id}/incidents/events`
 );
 
+// TRACKING
+
+const updateOnboardingStep = baseApi<
+  {
+    step: string;
+  },
+  {}
+>("POST", (pathParams) => {
+  return `/api/onboarding_step`;
+});
+
 // STACKS
 
 const createStack = baseApi<
@@ -2613,6 +2624,8 @@ export default {
   createContract,
   getContracts,
   deleteContract,
+  // TRACKING
+  updateOnboardingStep,
   // STACKS
   listStacks,
   getStack,

+ 5 - 0
go.work.sum

@@ -1,15 +1,20 @@
 cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
 cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
+github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
 github.com/containerd/stargz-snapshotter v0.11.3 h1:D3PoF563XmOBdtfx2G6AkhbHueqwIVPBFn2mrsWLa3w=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
 github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
 github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
 github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
+github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/51gbeLHfKGNfgEQQIWrlbdaOsidbQ=
 github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ=
 github.com/nats-io/nkeys v0.1.0 h1:qMd4+pRHgdr1nAClu+2h/2a5F2TmKcCzjCDazVgRoX4=
 github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
 github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck=
 golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=

+ 4 - 0
internal/analytics/track_events.go

@@ -8,6 +8,10 @@ const (
 	UserVerifyEmail SegmentEvent = "User Verified Email"
 	ProjectCreate   SegmentEvent = "New Project Event"
 
+	CostConsentComplete    SegmentEvent = "Cost Consent Complete"
+	CredentialStepComplete SegmentEvent = "Credential Step Complete"
+	ProvisioningAttempted  SegmentEvent = "Provisioning Attempted"
+
 	ClusterProvisioningStart   SegmentEvent = "Cluster Provisioning Started"
 	ClusterProvisioningError   SegmentEvent = "Cluster Provisioning Error"
 	ClusterProvisioningSuccess SegmentEvent = "Cluster Provisioning Success"

+ 51 - 1
internal/analytics/tracks.go

@@ -80,13 +80,18 @@ func (p segmentProperties) addAdditionalProperties(props map[string]interface{})
 type UserCreateTrackOpts struct {
 	*UserScopedTrackOpts
 
-	Email string
+	Email       string
+	FirstName   string
+	LastName    string
+	CompanyName string
 }
 
 // UserCreateTrack returns a track for when a user is created
 func UserCreateTrack(opts *UserCreateTrackOpts) segmentTrack {
 	additionalProps := make(map[string]interface{})
 	additionalProps["email"] = opts.Email
+	additionalProps["name"] = opts.FirstName + " " + opts.LastName
+	additionalProps["company"] = opts.CompanyName
 
 	return getSegmentUserTrack(
 		opts.UserScopedTrackOpts,
@@ -127,6 +132,51 @@ func ProjectCreateTrack(opts *ProjectCreateTrackOpts) segmentTrack {
 	)
 }
 
+// CostConsentTrackOpts are the options for creating a track when a user completes the cost consent
+type CostConsentTrackOpts struct {
+	*UserScopedTrackOpts
+}
+
+// CostConsentTrack returns a track for when a user completes the cost consent
+func CostConsentTrack(opts *CostConsentTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+
+	return getSegmentUserTrack(
+		opts.UserScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, CostConsentComplete),
+	)
+}
+
+// CredentialStepTrackOpts are the options for creating a track when a user completes the credential step
+type CredentialStepTrackOpts struct {
+	*UserScopedTrackOpts
+}
+
+// CredentialStepTrack returns a track for when a user completes the credential step
+func CredentialStepTrack(opts *CredentialStepTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+
+	return getSegmentUserTrack(
+		opts.UserScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, CredentialStepComplete),
+	)
+}
+
+// ProvisioningAttemptedTrackOpts are the options for creating a track when a user attempts provisioning
+type ProvisioningAttemptTrackOpts struct {
+	*UserScopedTrackOpts
+}
+
+// ProvisioningAttemptTrack returns a track for when a user attempts provisioning
+func ProvisioningAttemptTrack(opts *ProvisioningAttemptTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+
+	return getSegmentUserTrack(
+		opts.UserScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, ProvisioningAttempted),
+	)
+}
+
 // ClusterProvisioningStartTrackOpts are the options for creating a track when a cluster
 // has started provisioning
 type ClusterProvisioningStartTrackOpts struct {