Browse Source

small fixes (#4291)

Feroze Mohideen 2 years ago
parent
commit
3e489c6c0c

+ 4 - 1
api/server/handlers/api_contract/list.go

@@ -67,12 +67,15 @@ func (c *APIContractRevisionListHandler) ServeHTTP(w http.ResponseWriter, r *htt
 		telemetry.AttributeKV{Key: "cluster-id", Value: clusterID},
 		telemetry.AttributeKV{Key: "latest", Value: request.Latest},
 	)
+
+	resp := []*models.APIContractRevision{}
 	revisions, err := c.Config().Repo.APIContractRevisioner().List(ctx, proj.ID, repository.WithClusterID(uint(clusterID)), repository.WithLatest(request.Latest))
 	if err != nil {
 		err = telemetry.Error(ctx, span, err, "error getting latest api contract revisions")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
+	resp = append(resp, revisions...)
 
-	c.WriteResult(w, r, revisions)
+	c.WriteResult(w, r, resp)
 }

+ 43 - 0
api/server/handlers/project/update_onboarding_step.go

@@ -236,5 +236,48 @@ func (v *UpdateOnboardingStepHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		}
 	}
 
+	if request.Step == "cloud-provider-permissions-granted" {
+		err := v.Config().AnalyticsClient.Track(analytics.CloudProviderPermissionsGrantedTrack(&analytics.CloudProviderPermissionsGrantedTrackOpts{
+			ProjectScopedTrackOpts:            analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                             user.Email,
+			FirstName:                         user.FirstName,
+			LastName:                          user.LastName,
+			CompanyName:                       user.CompanyName,
+			CloudProvider:                     request.Provider,
+			CloudProviderCredentialIdentifier: request.CloudProviderCredentialIdentifier,
+		}))
+		if err != nil {
+			_ = telemetry.Error(ctx, span, err, "error tracking cloud provider permissions granted")
+		}
+	}
+
+	if request.Step == "cluster-preflight-checks-failed" {
+		err := v.Config().AnalyticsClient.Track(analytics.ClusterPreflightChecksFailedTrack(&analytics.ClusterPreflightChecksFailedTrackOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
+			ErrorMessage:           request.ErrorMessage,
+		}))
+		if err != nil {
+			_ = telemetry.Error(ctx, span, err, "error tracking cluster preflight checks failed")
+		}
+	}
+
+	if request.Step == "cluster-update-failed" {
+		err := v.Config().AnalyticsClient.Track(analytics.ClusterUpdateFailedTrack(&analytics.ClusterUpdateFailedTrackOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
+			ErrorMessage:           request.ErrorMessage,
+		}))
+		if err != nil {
+			_ = telemetry.Error(ctx, span, err, "error tracking cluster update failed")
+		}
+	}
+
 	v.WriteResult(w, r, user.ToUserType())
 }

+ 9 - 7
api/types/project.go

@@ -200,13 +200,15 @@ type UpdateOnboardingRequest OnboardingData
 
 // UpdateOnboardingStepRequest is a struct that contains the information needed to make a `POST projects/{project_id}/onboarding_step` request
 type UpdateOnboardingStepRequest struct {
-	Step              string `json:"step" form:"required,max=255"`
-	Provider          string `json:"provider"`
-	AccountId         string `json:"account_id"`
-	CloudformationURL string `json:"cloudformation_url"`
-	ErrorMessage      string `json:"error_message"`
-	LoginURL          string `json:"login_url"`
-	Region            string `json:"region"`
+	Step                              string `json:"step" form:"required,max=255"`
+	Provider                          string `json:"provider"`
+	CloudProviderCredentialIdentifier string `json:"cloud_provider_credential_identifier"`
+	AccountId                         string `json:"account_id"`
+	CloudformationURL                 string `json:"cloudformation_url"`
+	ErrorMessage                      string `json:"error_message"`
+	LoginURL                          string `json:"login_url"`
+	Region                            string `json:"region"`
+	ClusterName                       string `json:"cluster_name"`
 	// ExternalId used as a 'password' for the aws assume role chain to porter-manager role
 	ExternalId string `json:"external_id"`
 }

+ 1 - 1
dashboard/src/lib/clusters/types.ts

@@ -434,7 +434,7 @@ const eksConfigValidator = z.object({
       wafV2Arn: z.string(),
     })
     .default({
-      type: "UNKNOWN",
+      type: "NLB",
       wildcardDomain: "",
       allowlistIpRanges: "",
       certificateArns: [],

+ 81 - 0
dashboard/src/lib/hooks/useClusterAnalytics.ts

@@ -0,0 +1,81 @@
+import api from "shared/api";
+
+export const useClusterAnalytics = (): {
+  reportToAnalytics: ({
+    step,
+    projectId,
+    awsAccountId,
+    cloudFormationUrl,
+    errorMessage,
+    loginUrl,
+    externalId,
+    provider,
+    cloudProviderCredentialIdentifier,
+    region,
+    clusterName,
+  }: {
+    step: string;
+    projectId: number;
+    awsAccountId?: string;
+    cloudFormationUrl?: string;
+    errorMessage?: string;
+    loginUrl?: string;
+    externalId?: string;
+    provider?: string;
+    cloudProviderCredentialIdentifier?: string;
+    region?: string;
+    clusterName?: string;
+  }) => Promise<void>;
+} => {
+  const reportToAnalytics = async ({
+    step,
+    projectId,
+    awsAccountId = "",
+    cloudFormationUrl = "",
+    errorMessage = "",
+    loginUrl = "",
+    externalId = "",
+    region = "",
+    provider = "",
+    cloudProviderCredentialIdentifier = "",
+    clusterName = "",
+  }: {
+    step: string;
+    projectId: number;
+    awsAccountId?: string;
+    cloudFormationUrl?: string;
+    errorMessage?: string;
+    loginUrl?: string;
+    externalId?: string;
+    region?: string;
+    provider?: string;
+    cloudProviderCredentialIdentifier?: string;
+    clusterName?: string;
+  }): Promise<void> => {
+    await api
+      .updateOnboardingStep(
+        "<token>",
+        {
+          step,
+          account_id: awsAccountId,
+          cloudformation_url: cloudFormationUrl,
+          error_message: errorMessage,
+          login_url: loginUrl,
+          external_id: externalId,
+          region,
+          provider,
+          cloud_provider_credential_identifier:
+            cloudProviderCredentialIdentifier,
+          cluster_name: clusterName,
+        },
+        {
+          project_id: projectId,
+        }
+      )
+      .catch(() => ({})); // do not care about error here
+  };
+
+  return {
+    reportToAnalytics,
+  };
+};

+ 21 - 0
dashboard/src/main/home/infrastructure-dashboard/ClusterContextProvider.tsx

@@ -2,6 +2,7 @@ import React, { createContext, useCallback, useContext, useMemo } from "react";
 import { Contract } from "@porter-dev/api-contracts";
 import { useQueryClient } from "@tanstack/react-query";
 import styled from "styled-components";
+import { z } from "zod";
 
 import Loading from "components/Loading";
 import Container from "components/porter/Container";
@@ -10,11 +11,13 @@ import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import { updateExistingClusterContract } from "lib/clusters";
 import {
+  clusterValidator,
   type ClientCluster,
   type ClientClusterContract,
   type ClientNode,
 } from "lib/clusters/types";
 import { useCluster, useClusterNodeList } from "lib/hooks/useCluster";
+import { useClusterAnalytics } from "lib/hooks/useClusterAnalytics";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
@@ -56,6 +59,7 @@ const ClusterContextProvider: React.FC<ClusterContextProviderProps> = ({
     clusterId,
     refetchInterval: 3000,
   });
+  const { reportToAnalytics } = useClusterAnalytics();
 
   const { nodes } = useClusterNodeList({ clusterId });
 
@@ -115,6 +119,12 @@ const ClusterContextProvider: React.FC<ClusterContextProviderProps> = ({
     if (!paramsExist) {
       return;
     }
+
+    void reportToAnalytics({
+      projectId: currentProject.id,
+      step: "cluster-delete",
+    });
+
     await api.deleteCluster(
       "<token",
       {},
@@ -123,6 +133,17 @@ const ClusterContextProvider: React.FC<ClusterContextProviderProps> = ({
         cluster_id: clusterId,
       }
     );
+
+    const res = await api.getClusters("<token>", {}, { id: currentProject.id });
+    const parsed = await z.array(clusterValidator).parseAsync(res.data);
+    if (parsed.length === 0) {
+      await api.saveOnboardingState(
+        "<token>",
+        { current_step: "connect_source" },
+        { project_id: currentProject.id }
+      );
+    }
+
     await queryClient.invalidateQueries(["getClusters"]);
   }, [paramsExist, clusterId, currentProject?.id]);
   const isClusterUpdating = useMemo(() => {

+ 32 - 1
dashboard/src/main/home/infrastructure-dashboard/ClusterFormContextProvider.tsx

@@ -13,8 +13,11 @@ import {
   type UpdateClusterResponse,
 } from "lib/clusters/types";
 import { useUpdateCluster } from "lib/hooks/useCluster";
+import { useClusterAnalytics } from "lib/hooks/useClusterAnalytics";
 import { useIntercom } from "lib/hooks/useIntercom";
 
+import api from "shared/api";
+
 import PreflightChecksModal from "./modals/PreflightChecksModal";
 
 // todo(ianedwards): refactor button to use more predictable state
@@ -74,6 +77,8 @@ const ClusterFormContextProvider: React.FC<ClusterFormContextProviderProps> = ({
 
   const { showIntercomWithMessage } = useIntercom();
 
+  const { reportToAnalytics } = useClusterAnalytics();
+
   const clusterForm = useForm<ClientClusterContract>({
     reValidateMode: "onSubmit",
     resolver: zodResolver(clusterContractValidator),
@@ -123,16 +128,34 @@ const ClusterFormContextProvider: React.FC<ClusterFormContextProviderProps> = ({
   const onSubmit = handleSubmit(async (data) => {
     setUpdateClusterResponse(undefined);
     setUpdateClusterError("");
-    if (!currentContract?.cluster) {
+    if (!currentContract?.cluster || !projectId) {
       return;
     }
     try {
       const response = await updateCluster(data, currentContract);
       setUpdateClusterResponse(response);
       if (response.preflightChecks) {
+        void reportToAnalytics({
+          projectId,
+          step: "cluster-preflight-checks-failed",
+          errorMessage: `Preflight checks failed: ${response.preflightChecks
+            .map((c) => c.title)
+            .join(", ")}`,
+        });
         setShowFailedPreflightChecksModal(true);
       }
       if (response.createContractResponse) {
+        void reportToAnalytics({
+          projectId,
+          step: "provisioning-started",
+          provider: data.cluster.cloudProvider,
+          region: data.cluster.config.region,
+        });
+        await api.saveOnboardingState(
+          "<token>",
+          { current_step: "clean_up" },
+          { project_id: projectId }
+        );
         await queryClient.invalidateQueries(["getCluster"]);
 
         if (redirectOnSubmit) {
@@ -148,9 +171,17 @@ const ClusterFormContextProvider: React.FC<ClusterFormContextProviderProps> = ({
       }
     } catch (err) {
       if (err instanceof Error) {
+        void reportToAnalytics({
+          projectId,
+          step: "cluster-update-failed",
+          errorMessage: err.message,
+          provider: data.cluster.cloudProvider,
+          clusterName: data.cluster.config.clusterName,
+        });
         setUpdateClusterError(err.message);
         showIntercomWithMessage({
           message: "I am running into an issue updating my cluster.",
+          delaySeconds: 3,
         });
       }
     }

+ 0 - 1
dashboard/src/main/home/infrastructure-dashboard/forms/CloudProviderSelect.tsx

@@ -1,7 +1,6 @@
 import React, { useState } from "react";
 import styled from "styled-components";
 
-import Banner from "components/porter/Banner";
 import Button from "components/porter/Button";
 import DashboardPlaceholder from "components/porter/DashboardPlaceholder";
 import Link from "components/porter/Link";

+ 1 - 1
dashboard/src/main/home/infrastructure-dashboard/forms/aws/ConfigureEKSCluster.tsx

@@ -90,7 +90,7 @@ const ConfigureEKSCluster: React.FC<Props> = ({ goBack }) => {
           </>,
           isAdvancedSettingsEnabled ? (
             <>
-              <Text size={16}>CIDR Range</Text>
+              <Text size={16}>CIDR range</Text>
               <Spacer y={0.5} />
               <Text color="helper">
                 Specify the CIDR range for your cluster.

+ 8 - 0
dashboard/src/main/home/infrastructure-dashboard/forms/aws/CreateEKSClusterForm.tsx

@@ -4,6 +4,7 @@ import { match } from "ts-pattern";
 
 import { CloudProviderAWS } from "lib/clusters/constants";
 import { type ClientClusterContract } from "lib/clusters/types";
+import { useClusterAnalytics } from "lib/hooks/useClusterAnalytics";
 
 import { useClusterFormContext } from "../../ClusterFormContextProvider";
 import ConfigureEKSCluster from "./ConfigureEKSCluster";
@@ -22,6 +23,7 @@ const CreateEKSClusterForm: React.FC<Props> = ({
   const [step, setStep] = useState<"permissions" | "cluster">("permissions");
   const { setValue, reset } = useFormContext<ClientClusterContract>();
   const { setCurrentContract } = useClusterFormContext();
+  const { reportToAnalytics } = useClusterAnalytics();
 
   useEffect(() => {
     reset({
@@ -74,6 +76,12 @@ const CreateEKSClusterForm: React.FC<Props> = ({
             "cluster.cloudProviderCredentialsId",
             cloudProviderCredentialIdentifier
           );
+          void reportToAnalytics({
+            projectId,
+            step: "cloud-provider-permissions-granted",
+            provider: CloudProviderAWS.name,
+            cloudProviderCredentialIdentifier,
+          });
           setStep("cluster");
         }}
         projectId={projectId}

+ 28 - 63
dashboard/src/main/home/infrastructure-dashboard/forms/aws/GrantAWSPermissions.tsx

@@ -14,8 +14,7 @@ import Text from "components/porter/Text";
 import VerticalSteps from "components/porter/VerticalSteps";
 import { CloudProviderAWS } from "lib/clusters/constants";
 import { isAWSArnAccessible } from "lib/hooks/useCloudProvider";
-
-import api from "shared/api";
+import { useClusterAnalytics } from "lib/hooks/useClusterAnalytics";
 
 import GrantAWSPermissionsHelpModal from "../../modals/help/permissions/GrantAWSPermissionsHelpModal";
 import { CheckItem } from "../../modals/PreflightChecksModal";
@@ -41,41 +40,7 @@ const GrantAWSPermissions: React.FC<Props> = ({
   const [accountIdContinueButtonStatus, setAccountIdContinueButtonStatus] =
     useState<string>("");
   const [isAccountAccessible, setIsAccountAccessible] = useState(false);
-  const reportToAnalytics = useCallback(
-    async ({
-      step,
-      awsAccountId = "",
-      cloudFormationUrl = "",
-      errorMessage = "",
-      loginUrl = "",
-      externalId = "",
-    }: {
-      step: string;
-      awsAccountId?: string;
-      cloudFormationUrl?: string;
-      errorMessage?: string;
-      loginUrl?: string;
-      externalId?: string;
-    }) => {
-      void api
-        .updateOnboardingStep(
-          "<token>",
-          {
-            step,
-            account_id: awsAccountId,
-            cloudformation_url: cloudFormationUrl,
-            error_message: errorMessage,
-            login_url: loginUrl,
-            external_id: externalId,
-          },
-          {
-            project_id: projectId,
-          }
-        )
-        .catch(() => ({})); // do not care about error here, so just catch it
-    },
-    [projectId]
-  );
+  const { reportToAnalytics } = useClusterAnalytics();
   const awsAccountIdInputError = useMemo(() => {
     const regex = /^\d{12}$/;
     if (AWSAccountID.trim().length === 0) {
@@ -141,6 +106,7 @@ const GrantAWSPermissions: React.FC<Props> = ({
       setCurrentStep(2);
     }
     void reportToAnalytics({
+      projectId,
       step: "aws-account-id-complete",
       awsAccountId: AWSAccountID,
     });
@@ -149,6 +115,7 @@ const GrantAWSPermissions: React.FC<Props> = ({
   const directToAWSLogin = (): void => {
     const loginUrl = `https://signin.aws.amazon.com/console`;
     void reportToAnalytics({
+      projectId,
       step: "aws-login-redirect-success",
       loginUrl,
     });
@@ -160,6 +127,7 @@ const GrantAWSPermissions: React.FC<Props> = ({
       : "arn:aws:iam::108458755588:role/CAPIManagement";
     const cloudFormationUrl = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-access-policy.json&stackName=PorterRole&param_TrustArnParameter=${trustArn}`;
     void reportToAnalytics({
+      projectId,
       step: "aws-cloudformation-redirect-success",
       awsAccountId: AWSAccountID,
       cloudFormationUrl,
@@ -169,10 +137,6 @@ const GrantAWSPermissions: React.FC<Props> = ({
     window.open(cloudFormationUrl, "_blank");
   }, [AWSAccountID, externalId]);
   const handleGrantPermissionsComplete = (): void => {
-    void reportToAnalytics({
-      step: "aws-create-integration-success",
-      awsAccountId: AWSAccountID,
-    });
     proceed({
       cloudProviderCredentialIdentifier: `arn:aws:iam::${AWSAccountID}:role/porter-manager`,
     });
@@ -253,6 +217,16 @@ const GrantAWSPermissions: React.FC<Props> = ({
             />
             <Spacer y={1} />
             <StepChangeButtonsContainer>
+              <Button
+                onClick={() => {
+                  setCurrentStep(0);
+                }}
+                color="#222222"
+              >
+                Back
+              </Button>
+              <Spacer inline x={0.5} />
+
               <Button
                 onClick={checkIfAlreadyAccessible}
                 disabled={
@@ -260,19 +234,10 @@ const GrantAWSPermissions: React.FC<Props> = ({
                   AWSAccountID.length === 0 ||
                   accountIdContinueButtonStatus === "loading"
                 }
-              >
-                Continue
-              </Button>
-              <Spacer inline x={0.5} />
-              <Button
-                onClick={() => {
-                  setCurrentStep(0);
-                }}
-                color="#222222"
                 status={accountIdContinueButtonStatus}
                 loadingText={`Checking if Porter can already access this account`}
               >
-                Back
+                Continue
               </Button>
             </StepChangeButtonsContainer>
           </>,
@@ -309,19 +274,19 @@ const GrantAWSPermissions: React.FC<Props> = ({
             <StepChangeButtonsContainer>
               <Button
                 onClick={() => {
-                  setCurrentStep(3);
+                  setCurrentStep(1);
                 }}
+                color="#222222"
               >
-                Continue
+                Back
               </Button>
               <Spacer inline x={0.5} />
               <Button
                 onClick={() => {
-                  setCurrentStep(1);
+                  setCurrentStep(3);
                 }}
-                color="#222222"
               >
-                Back
+                Continue
               </Button>
             </StepChangeButtonsContainer>
           </>,
@@ -350,13 +315,6 @@ const GrantAWSPermissions: React.FC<Props> = ({
             />
             <Spacer y={1} />
             <Container row>
-              <Button
-                onClick={handleGrantPermissionsComplete}
-                disabled={!isAccountAccessible}
-              >
-                Continue
-              </Button>
-              <Spacer inline x={0.5} />
               <Button
                 onClick={() => {
                   setCurrentStep(2);
@@ -365,6 +323,13 @@ const GrantAWSPermissions: React.FC<Props> = ({
               >
                 Back
               </Button>
+              <Spacer inline x={0.5} />
+              <Button
+                onClick={handleGrantPermissionsComplete}
+                disabled={!isAccountAccessible}
+              >
+                Continue
+              </Button>
             </Container>
           </>,
         ]}

+ 12 - 1
dashboard/src/main/home/infrastructure-dashboard/forms/azure/ConfigureAKSCluster.tsx

@@ -98,6 +98,17 @@ const ConfigureAKSCluster: React.FC<Props> = ({ goBack }) => {
             <Container style={{ width: "300px" }}>
               <Text size={16}>Azure tier</Text>
               <Spacer y={0.5} />
+              <Text color="helper">
+                Select Azure cluster management tier.{" "}
+                <a
+                  href="https://learn.microsoft.com/en-us/azure/aks/free-standard-pricing-tiers"
+                  target="_blank"
+                  rel="noreferrer"
+                >
+                  &nbsp;(?)
+                </a>
+              </Text>
+              <Spacer y={0.7} />
               <Controller
                 name={`cluster.config.skuTier`}
                 control={control}
@@ -118,7 +129,7 @@ const ConfigureAKSCluster: React.FC<Props> = ({ goBack }) => {
           </>,
           isAdvancedSettingsEnabled ? (
             <>
-              <Text size={16}>CIDR Range</Text>
+              <Text size={16}>CIDR range</Text>
               <Spacer y={0.5} />
               <Text color="helper">
                 Specify the CIDR range for your cluster.

+ 8 - 0
dashboard/src/main/home/infrastructure-dashboard/forms/azure/CreateAKSClusterForm.tsx

@@ -4,6 +4,7 @@ import { match } from "ts-pattern";
 
 import { CloudProviderAzure } from "lib/clusters/constants";
 import { type ClientClusterContract } from "lib/clusters/types";
+import { useClusterAnalytics } from "lib/hooks/useClusterAnalytics";
 
 import { useClusterFormContext } from "../../ClusterFormContextProvider";
 import ConfigureAKSCluster from "./ConfigureAKSCluster";
@@ -23,6 +24,7 @@ const CreateAKSClusterForm: React.FC<Props> = ({
 
   const { setValue, reset } = useFormContext<ClientClusterContract>();
   const { setCurrentContract } = useClusterFormContext();
+  const { reportToAnalytics } = useClusterAnalytics();
 
   useEffect(() => {
     reset({
@@ -76,6 +78,12 @@ const CreateAKSClusterForm: React.FC<Props> = ({
             "cluster.cloudProviderCredentialsId",
             cloudProviderCredentialIdentifier
           );
+          void reportToAnalytics({
+            projectId,
+            step: "cloud-provider-permissions-granted",
+            provider: CloudProviderAzure.name,
+            cloudProviderCredentialIdentifier,
+          });
           setStep("cluster");
         }}
         projectId={projectId}

+ 1 - 1
dashboard/src/main/home/infrastructure-dashboard/forms/gcp/ConfigureGKECluster.tsx

@@ -95,7 +95,7 @@ const ConfigureGKECluster: React.FC<Props> = ({ goBack }) => {
           </>,
           isAdvancedSettingsEnabled ? (
             <>
-              <Text size={16}>CIDR Range</Text>
+              <Text size={16}>CIDR range</Text>
               <Spacer y={0.5} />
               <Text color="helper">
                 Specify the CIDR range for your cluster.

+ 8 - 0
dashboard/src/main/home/infrastructure-dashboard/forms/gcp/CreateGKEClusterForm.tsx

@@ -4,6 +4,7 @@ import { match } from "ts-pattern";
 
 import { CloudProviderGCP } from "lib/clusters/constants";
 import { type ClientClusterContract } from "lib/clusters/types";
+import { useClusterAnalytics } from "lib/hooks/useClusterAnalytics";
 
 import { useClusterFormContext } from "../../ClusterFormContextProvider";
 import ConfigureGKECluster from "./ConfigureGKECluster";
@@ -24,6 +25,7 @@ const CreateGKEClusterForm: React.FC<Props> = ({
 
   const { setValue, reset } = useFormContext<ClientClusterContract>();
   const { setCurrentContract } = useClusterFormContext();
+  const { reportToAnalytics } = useClusterAnalytics();
 
   useEffect(() => {
     reset({
@@ -76,6 +78,12 @@ const CreateGKEClusterForm: React.FC<Props> = ({
             "cluster.cloudProviderCredentialsId",
             cloudProviderCredentialIdentifier
           );
+          void reportToAnalytics({
+            projectId,
+            step: "cloud-provider-permissions-granted",
+            provider: CloudProviderGCP.name,
+            cloudProviderCredentialIdentifier,
+          });
           setStep("cluster");
         }}
         projectId={projectId}

+ 2 - 1
dashboard/src/main/home/infrastructure-dashboard/shared/advanced/EKSClusterAdvancedSettings.tsx

@@ -104,7 +104,8 @@ const EKSClusterAdvancedSettings: React.FC = () => {
       <Text size={16}>AWS CloudWatch logging</Text>
       <Spacer y={0.5} />
       <Text color={"helper"}>
-        Configure which logs to send to AWS CloudWatch.
+        Configure which EKS cluster control plane log types to send to AWS
+        CloudWatch.
       </Text>
       <Spacer y={0.5} />
       <SettingsGroupContainer>

+ 7 - 4
dashboard/src/shared/api.tsx

@@ -585,10 +585,11 @@ const createProject = baseApi<{ name: string }, {}>("POST", (pathParams) => {
 });
 
 const connectProjectToCluster = baseApi<
-{}, 
-{
-  id: number;
-}>("POST", (pathParams) => {
+  {},
+  {
+    id: number;
+  }
+>("POST", (pathParams) => {
   const { id } = pathParams;
 
   return `/api/projects/${id}/connect`;
@@ -3213,12 +3214,14 @@ const updateOnboardingStep = baseApi<
   {
     step: string;
     provider?: string;
+    cloud_provider_credential_identifier?: string;
     account_id?: string;
     cloudformation_url?: string;
     error_message?: string;
     login_url?: string;
     external_id?: string;
     region?: string;
+    cluster_name?: string;
   },
   {
     project_id: number;

+ 9 - 0
internal/analytics/track_events.go

@@ -62,4 +62,13 @@ const (
 	StackBuildSuccess     SegmentEvent = "Stack Build Success"
 
 	PorterAppUpdateFailure SegmentEvent = "Porter App Update Failure"
+
+	// new infra flow
+
+	// CloudProviderPermissionsGranted is a segment event that is triggered when a user grants cloud provider permissions
+	CloudProviderPermissionsGranted SegmentEvent = "Cloud Provider Permissions Granted"
+	// ClusterPreflightChecksFailed is a segment event that is triggered when a user's cluster fails preflight checks
+	ClusterPreflightChecksFailed SegmentEvent = "Cluster Preflight Checks Failed"
+	// ClusterUpdateFailed is a segment event that is triggered when a user's cluster update fails
+	ClusterUpdateFailed SegmentEvent = "Cluster Update Failed"
 )

+ 79 - 0
internal/analytics/tracks.go

@@ -1036,3 +1036,82 @@ func PorterAppUpdateFailureTrack(opts *PorterAppUpdateOpts) segmentTrack {
 		getDefaultSegmentTrack(additionalProps, PorterAppUpdateFailure),
 	)
 }
+
+// CloudProviderPermissionsGrantedTrackOpts are the options for creating a track when a user grants permission to use porter
+type CloudProviderPermissionsGrantedTrackOpts struct {
+	*ProjectScopedTrackOpts
+
+	Email                             string
+	FirstName                         string
+	LastName                          string
+	CompanyName                       string
+	CloudProvider                     string
+	CloudProviderCredentialIdentifier string
+}
+
+// CloudProviderPermissionsGrantedTrack returns a track for when a user grants permission to use porter
+func CloudProviderPermissionsGrantedTrack(opts *CloudProviderPermissionsGrantedTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["email"] = opts.Email
+	additionalProps["name"] = opts.FirstName + " " + opts.LastName
+	additionalProps["company"] = opts.CompanyName
+	additionalProps["cloud_provider"] = opts.CloudProvider
+	additionalProps["cloud_provider_credential_identifier"] = opts.CloudProviderCredentialIdentifier
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, CloudProviderPermissionsGranted),
+	)
+}
+
+// ClusterPreflightChecksFailedTrackOpts are the options for creating a track when a user fails preflight checks
+type ClusterPreflightChecksFailedTrackOpts struct {
+	*ProjectScopedTrackOpts
+
+	Email        string
+	FirstName    string
+	LastName     string
+	CompanyName  string
+	ErrorMessage string
+}
+
+// ClusterPreflightChecksFailedTrack returns a track for when a user fails preflight checks
+func ClusterPreflightChecksFailedTrack(opts *ClusterPreflightChecksFailedTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["email"] = opts.Email
+	additionalProps["name"] = opts.FirstName + " " + opts.LastName
+	additionalProps["company"] = opts.CompanyName
+	additionalProps["error_message"] = opts.ErrorMessage
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, ClusterPreflightChecksFailed),
+	)
+}
+
+// ClusterUpdateFailedTrackOpts are the options for creating a track when a user fails to update a cluster
+type ClusterUpdateFailedTrackOpts struct {
+	*ProjectScopedTrackOpts
+
+	ClusterName  string
+	Email        string
+	FirstName    string
+	LastName     string
+	CompanyName  string
+	ErrorMessage string
+}
+
+// ClusterUpdateFailedTrack returns a track for when a user fails to update a cluster
+func ClusterUpdateFailedTrack(opts *ClusterUpdateFailedTrackOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["cluster_name"] = opts.ClusterName
+	additionalProps["email"] = opts.Email
+	additionalProps["name"] = opts.FirstName + " " + opts.LastName
+	additionalProps["company"] = opts.CompanyName
+	additionalProps["error_message"] = opts.ErrorMessage
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, ClusterUpdateFailed),
+	)
+}