فهرست منبع

add analytics (#3021)

Co-authored-by: jusrhee <justin@porter.run>
Feroze Mohideen 3 سال پیش
والد
کامیت
ba61a48a56

+ 59 - 0
api/server/handlers/stacks/porter_app_analytics.go

@@ -0,0 +1,59 @@
+package stacks
+
+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 PorterAppAnalyticsHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewPorterAppAnalyticsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *PorterAppAnalyticsHandler {
+	return &PorterAppAnalyticsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (v *PorterAppAnalyticsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	user, _ := ctx.Value(types.UserScope).(*models.User)
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	request := &types.PorterAppAnalyticsRequest{}
+	if ok := v.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	if request.Step == "stack-launch-start" {
+		v.Config().AnalyticsClient.Track(analytics.StackLaunchStartTrack(&analytics.StackLaunchStartOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+		}))
+	}
+
+	if request.Step == "stack-launch-complete" {
+		v.Config().AnalyticsClient.Track(analytics.StackLaunchCompleteTrack(&analytics.StackLaunchCompleteOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			StackName:              request.StackName,
+		}))
+	}
+
+	if request.Step == "stack-launch-success" {
+		v.Config().AnalyticsClient.Track(analytics.StackLaunchSuccessTrack(&analytics.StackLaunchSuccessOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			StackName:              request.StackName,
+		}))
+	}
+
+	v.WriteResult(w, r, user.ToUserType())
+}

+ 29 - 0
api/server/router/stack.go

@@ -198,5 +198,34 @@ func getStackRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/analytics -> stacks.NewPorterAppAnalyticsHandler
+	porterAppAnalyticsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/analytics", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	porterAppAnalyticsHandler := stacks.NewPorterAppAnalyticsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: porterAppAnalyticsEndpoint,
+		Handler:  porterAppAnalyticsHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 5 - 0
api/types/stack.go

@@ -24,3 +24,8 @@ type CreateSecretAndOpenGHPRResponse struct {
 }
 
 type GetStackResponse PorterApp
+
+type PorterAppAnalyticsRequest struct {
+	Step      string `json:"step" form:"required,max=255"`
+	StackName string `json:"stack_name"`
+}

+ 20 - 2
dashboard/src/main/home/app-dashboard/AppDashboard.tsx

@@ -48,7 +48,7 @@ const namespaceBlacklist = [
   "monitoring",
 ];
 
-const AppDashboard: React.FC<Props> = ({}) => {
+const AppDashboard: React.FC<Props> = ({ }) => {
   const { currentProject, currentCluster } = useContext(Context);
   const [apps, setApps] = useState([]);
   const [charts, setCharts] = useState([]);
@@ -146,6 +146,24 @@ const AppDashboard: React.FC<Props> = ({}) => {
     );
   };
 
+  const updateStackStartedStep = async () => {
+    try {
+      await api.updateStackStep(
+        "<token>",
+        {
+          step: 'stack-launch-start'
+        },
+        {
+          cluster_id: currentCluster.id,
+          project_id: currentProject.id,
+        }
+      );
+    } catch (err) {
+      // TODO: handle error
+    }
+  }
+
+
   const renderIcon = (b: string, size?: string) => {
     var src = box;
     if (b) {
@@ -198,7 +216,7 @@ const AppDashboard: React.FC<Props> = ({}) => {
         />
         <Spacer inline x={2} />
         <PorterLink to="/apps/new/app">
-          <Button onClick={() => {}} height="30px" width="160px">
+          <Button onClick={async () => updateStackStartedStep()} height="30px" width="160px">
             <I className="material-icons">add</I> New application
           </Button>
         </PorterLink>

+ 27 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -153,6 +153,25 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       (error || !accessData.accounts || accessData.accounts?.length === 0)
     );
   };
+
+  const updateStackStep = async (step: string) => {
+    try {
+      await api.updateStackStep(
+        "<token>",
+        {
+          step,
+          stack_name: formState.applicationName,
+        },
+        {
+          cluster_id: currentCluster.id,
+          project_id: currentProject.id,
+        }
+      );
+    } catch (err) {
+      // TODO: handle analytics error
+    }
+  }
+
   const validatePorterYaml = (yamlString: string) => {
     let parsedYaml;
     try {
@@ -287,6 +306,10 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     try {
       setDeploying(true);
       setDeploymentError(undefined);
+
+      // log analytics event that we started form submission
+      await updateStackStep('stack-launch-complete');
+
       if (
         currentProject == null ||
         currentCluster == null ||
@@ -343,6 +366,10 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       if (!actionConfig?.git_repo) {
         props.history.push(`/apps/${formState.applicationName}`);
       }
+
+      // log analytics event that we successfully deployed
+      await updateStackStep('stack-launch-success');
+
       return true;
     } catch (err) {
       // TODO: better error handling

+ 1 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/ReleaseTabs.tsx

@@ -40,7 +40,7 @@ const ReleaseTabs: React.FC<Props> = ({
             <>
                 <Spacer y={1} />
                 <Input
-                    label="CPUs (Mi)"
+                    label="CPU (Millicores)"
                     placeholder="ex: 0.5"
                     value={service.cpu.value}
                     disabled={service.cpu.readOnly}

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

@@ -2300,6 +2300,20 @@ const updateOnboardingStep = baseApi<
   return `/api/onboarding_step`;
 });
 
+const updateStackStep = baseApi<
+  {
+    step: string;
+    stack_name?: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+  }
+>("POST", (pathParams) => {
+  let { project_id, cluster_id } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/analytics`;
+});
+
 // STACKS
 
 const createStack = baseApi<
@@ -2705,6 +2719,7 @@ export default {
   createSecretAndOpenGitHubPullRequest,
   // TRACKING
   updateOnboardingStep,
+  updateStackStep,
   // STACKS
   listStacks,
   getStack,

+ 5 - 0
internal/analytics/track_events.go

@@ -39,4 +39,9 @@ const (
 	// delete events
 	ClusterDestroyingStart   SegmentEvent = "Cluster Destroying Start"
 	ClusterDestroyingSuccess SegmentEvent = "Cluster Destroying Success"
+
+	// stacks
+	StackLaunchStart    SegmentEvent = "Stack Launch Started"
+	StackLaunchComplete SegmentEvent = "Stack Launch Complete"
+	StackLaunchSuccess  SegmentEvent = "Stack Launch Success"
 )

+ 49 - 0
internal/analytics/tracks.go

@@ -567,3 +567,52 @@ func ClusterDestroyingSuccessTrack(opts *ClusterDestroyingSuccessTrackOpts) segm
 		getDefaultSegmentTrack(additionalProps, ClusterDestroyingSuccess),
 	)
 }
+
+// StackLaunchStartOpts are the options for creating a track when a user starts creating a stack
+type StackLaunchStartOpts struct {
+	*ProjectScopedTrackOpts
+}
+
+// StackLaunchStartTrack returns a track for when a user starts creating a stack
+func StackLaunchStartTrack(opts *StackLaunchStartOpts) segmentTrack {
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(nil, StackLaunchStart),
+	)
+}
+
+// StackLaunchCompleteOpts are the options for creating a track when a user completes creating a stack
+type StackLaunchCompleteOpts struct {
+	*ProjectScopedTrackOpts
+
+	StackName string
+}
+
+// StackLaunchCompleteTrack returns a track for when a user completes creating a stack
+func StackLaunchCompleteTrack(opts *StackLaunchCompleteOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["stack_name"] = opts.StackName
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(nil, StackLaunchComplete),
+	)
+}
+
+// StackLaunchSuccessOpts are the options for creating a track when a user succeeds in creating a stack
+type StackLaunchSuccessOpts struct {
+	*ProjectScopedTrackOpts
+
+	StackName string
+}
+
+// StackLaunchCompleteTrack returns a track for when a user completes creating a stack
+func StackLaunchSuccessTrack(opts *StackLaunchSuccessOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["stack_name"] = opts.StackName
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(nil, StackLaunchSuccess),
+	)
+}

+ 0 - 2
internal/kubernetes/config.go

@@ -402,8 +402,6 @@ func (conf *OutOfClusterConfig) CreateRawConfigFromCluster() (*api.Config, error
 
 		authInfoMap[authInfoName].Token = tok
 
-		fmt.Printf("I successfully ran!")
-
 	} else {
 		switch cluster.AuthMechanism {
 		case models.X509: