Przeglądaj źródła

cost consent and provision attempt analytics

Justin Rhee 3 lat temu
rodzic
commit
2cdf33fa77

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

@@ -163,14 +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,
-				FirstName:           user.FirstName,
-				LastName:            user.LastName,
-				CompanyName:         user.CompanyName,
-			}))
 		} else if err == nil {
 			return nil, fmt.Errorf("email already registered")
 		} else if err != nil {

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

@@ -148,14 +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,
-				FirstName:           user.FirstName,
-				LastName:            user.LastName,
-				CompanyName:         user.CompanyName,
-			}))
 		} 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");
               }}
             >

+ 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,

+ 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"

+ 45 - 0
internal/analytics/tracks.go

@@ -132,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 {