Explorar el Código

Changing the order of cloudformation input form and adding event for build failures (#3278)

Feroze Mohideen hace 2 años
padre
commit
142cb3fc21

+ 12 - 0
api/server/handlers/porter_app/analytics.go

@@ -92,3 +92,15 @@ func (v *PorterAppAnalyticsHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 
 	v.WriteResult(w, r, user.ToUserType())
 }
+
+func TrackStackBuildFailure(
+	config *config.Config,
+	user *models.User,
+	project *models.Project,
+	stackName string,
+) error {
+	return config.AnalyticsClient.Track(analytics.StackBuildFailureTrack(&analytics.StackBuildFailureOpts{
+		ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+		StackName:              stackName,
+	}))
+}

+ 20 - 8
api/server/handlers/porter_app/list_events.go

@@ -42,10 +42,8 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	defer span.End()
 
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "cluster-id", Value: int(cluster.ID)},
-		telemetry.AttributeKV{Key: "project-id", Value: int(cluster.ProjectID)},
-	)
+	user, _ := ctx.Value(types.UserScope).(*models.User)
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
 	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
 	if reqErr != nil {
@@ -80,7 +78,7 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 
 	for idx, appEvent := range porterAppEvents {
 		if appEvent.Status == "PROGRESSING" {
-			pae, err := p.updateExistingAppEvent(ctx, *cluster, stackName, *appEvent)
+			pae, err := p.updateExistingAppEvent(ctx, *cluster, stackName, *appEvent, user, project)
 			if err != nil {
 				e := telemetry.Error(ctx, span, nil, "unable to update existing porter app event")
 				p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
@@ -108,7 +106,14 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	p.WriteResult(w, r, res)
 }
 
-func (p *PorterAppEventListHandler) updateExistingAppEvent(ctx context.Context, cluster models.Cluster, stackName string, appEvent models.PorterAppEvent) (models.PorterAppEvent, error) {
+func (p *PorterAppEventListHandler) updateExistingAppEvent(
+	ctx context.Context,
+	cluster models.Cluster,
+	stackName string,
+	appEvent models.PorterAppEvent,
+	user *models.User,
+	project *models.Project,
+) (models.PorterAppEvent, error) {
 	ctx, span := telemetry.NewSpan(ctx, "update-porter-app-event")
 	defer span.End()
 
@@ -130,7 +135,7 @@ func (p *PorterAppEventListHandler) updateExistingAppEvent(ctx context.Context,
 	)
 
 	if appEvent.Type == string(types.PorterAppEventType_Build) && appEvent.TypeExternalSource == "GITHUB" {
-		err = p.updateBuildEvent_Github(ctx, &event)
+		err = p.updateBuildEvent_Github(ctx, &event, user, project, stackName)
 		if err != nil {
 			return models.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error updating porter app event for github build")
 		}
@@ -150,7 +155,13 @@ func (p *PorterAppEventListHandler) updateExistingAppEvent(ctx context.Context,
 	return event, nil
 }
 
-func (p *PorterAppEventListHandler) updateBuildEvent_Github(ctx context.Context, event *models.PorterAppEvent) error {
+func (p *PorterAppEventListHandler) updateBuildEvent_Github(
+	ctx context.Context,
+	event *models.PorterAppEvent,
+	user *models.User,
+	project *models.Project,
+	stackName string,
+) error {
 	ctx, span := telemetry.NewSpan(ctx, "update-porter-app-build-event")
 	defer span.End()
 
@@ -214,6 +225,7 @@ func (p *PorterAppEventListHandler) updateBuildEvent_Github(ctx context.Context,
 			event.Status = "SUCCESS"
 		} else {
 			event.Status = "FAILED"
+			_ = TrackStackBuildFailure(p.Config(), user, project, stackName)
 		}
 		event.Metadata["end_time"] = actionRun.GetUpdatedAt().Time
 	}

+ 60 - 46
api/server/handlers/user/update_onboarding_step.go → api/server/handlers/project/update_onboarding_step.go

@@ -1,4 +1,4 @@
-package user
+package project
 
 import (
 	"net/http"
@@ -27,6 +27,7 @@ func NewUpdateOnboardingStepHandler(
 
 func (v *UpdateOnboardingStepHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
 	request := &types.UpdateOnboardingStepRequest{}
 	if ok := v.DecodeAndValidate(w, r, request); !ok {
@@ -57,61 +58,61 @@ func (v *UpdateOnboardingStepHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 
 	if request.Step == "aws-account-id-complete" {
 		v.Config().AnalyticsClient.Track(analytics.AWSInputTrack(&analytics.AWSInputTrackOpts{
-			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
-			Email:               user.Email,
-			FirstName:           user.FirstName,
-			LastName:            user.LastName,
-			CompanyName:         user.CompanyName,
-			AccountId:           request.AccountId,
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
+			AccountId:              request.AccountId,
 		}))
 	}
 
 	if request.Step == "aws-login-redirect-success" {
 		v.Config().AnalyticsClient.Track(analytics.AWSLoginRedirectSuccess(&analytics.AWSRedirectOpts{
-			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
-			Email:               user.Email,
-			FirstName:           user.FirstName,
-			LastName:            user.LastName,
-			CompanyName:         user.CompanyName,
-			AccountId:           request.AccountId,
-			LoginURL:            request.LoginURL,
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
+			AccountId:              request.AccountId,
+			LoginURL:               request.LoginURL,
 		}))
 	}
 
 	if request.Step == "aws-cloudformation-redirect-success" {
 		v.Config().AnalyticsClient.Track(analytics.AWSCloudformationRedirectSuccess(&analytics.AWSRedirectOpts{
-			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
-			Email:               user.Email,
-			FirstName:           user.FirstName,
-			LastName:            user.LastName,
-			CompanyName:         user.CompanyName,
-			AccountId:           request.AccountId,
-			CloudformationURL:   request.CloudformationURL,
-			ExternalId:          request.ExternalId,
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
+			AccountId:              request.AccountId,
+			CloudformationURL:      request.CloudformationURL,
+			ExternalId:             request.ExternalId,
 		}))
 	}
 
 	if request.Step == "aws-create-integration-success" {
 		v.Config().AnalyticsClient.Track(analytics.AWSCreateIntegrationSucceeded(&analytics.AWSCreateIntegrationOpts{
-			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
-			Email:               user.Email,
-			FirstName:           user.FirstName,
-			LastName:            user.LastName,
-			CompanyName:         user.CompanyName,
-			AccountId:           request.AccountId,
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
+			AccountId:              request.AccountId,
 		}))
 	}
 
 	if request.Step == "aws-create-integration-failure" {
 		v.Config().AnalyticsClient.Track(analytics.AWSCreateIntegrationFailed(&analytics.AWSCreateIntegrationOpts{
-			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
-			Email:               user.Email,
-			FirstName:           user.FirstName,
-			LastName:            user.LastName,
-			CompanyName:         user.CompanyName,
-			AccountId:           request.AccountId,
-			ErrorMessage:        request.ErrorMessage,
-			ExternalId:          request.ExternalId,
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
+			AccountId:              request.AccountId,
+			ErrorMessage:           request.ErrorMessage,
+			ExternalId:             request.ExternalId,
 		}))
 	}
 
@@ -123,21 +124,34 @@ func (v *UpdateOnboardingStepHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 
 	if request.Step == "pre-provisioning-check-started" {
 		v.Config().AnalyticsClient.Track(analytics.PreProvisionCheckTrack(&analytics.PreProvisionCheckTrackOpts{
-			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
-			Email:               user.Email,
-			FirstName:           user.FirstName,
-			LastName:            user.LastName,
-			CompanyName:         user.CompanyName,
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
 		}))
 	}
 
 	if request.Step == "provisioning-started" {
 		v.Config().AnalyticsClient.Track(analytics.ProvisioningAttemptTrack(&analytics.ProvisioningAttemptTrackOpts{
-			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
-			Email:               user.Email,
-			FirstName:           user.FirstName,
-			LastName:            user.LastName,
-			CompanyName:         user.CompanyName,
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
+			Region:                 request.Region,
+		}))
+	}
+
+	if request.Step == "provisioning-failed" {
+		v.Config().AnalyticsClient.Track(analytics.ProvisionFailureTrack(&analytics.ProvisioningAttemptTrackOpts{
+			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, project.ID),
+			Email:                  user.Email,
+			FirstName:              user.FirstName,
+			LastName:               user.LastName,
+			CompanyName:            user.CompanyName,
+			ErrorMessage:           request.ErrorMessage,
+			Region:                 request.Region,
 		}))
 	}
 

+ 28 - 0
api/server/router/project.go

@@ -201,6 +201,34 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/onboarding_step -> project.UpdateOnboardingStepHandler
+	updateOnboardingStepEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/onboarding_step",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	updateOnboardingStepHandler := project.NewUpdateOnboardingStepHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: updateOnboardingStepEndpoint,
+		Handler:  updateOnboardingStepHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/invite_admin -> project.NewProjectInviteAdminHandler
 	projectInviteAdminEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

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

@@ -146,31 +146,6 @@ 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{

+ 12 - 0
api/types/project.go

@@ -124,3 +124,15 @@ type OnboardingData struct {
 }
 
 type UpdateOnboardingRequest OnboardingData
+
+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"`
+	// ExternalId used as a 'password' for the aws assume role chain to porter-manager role
+	ExternalId string `json:"external_id"`
+}

+ 0 - 11
api/types/user.go

@@ -80,14 +80,3 @@ 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"`
-	Provider          string `json:"provider"`
-	AccountId         string `json:"account_id"`
-	CloudformationURL string `json:"cloudformation_url"`
-	ErrorMessage      string `json:"error_message"`
-	LoginURL          string `json:"login_url"`
-	// used as a 'password' for the aws assume role chain to porter-manager role
-	ExternalId string `json:"external_id"`
-}

+ 109 - 107
dashboard/src/components/AzureProvisionerSettings.tsx

@@ -76,7 +76,9 @@ const AzureProvisionerSettings: React.FC<Props> = (props) => {
 
   const markStepStarted = async (step: string) => {
     try {
-      await api.updateOnboardingStep("<token>", { step }, {});
+      await api.updateOnboardingStep("<token>", { step }, {
+        project_id: currentProject.id,
+      });
     } catch (err) {
       console.log(err);
     }
@@ -91,8 +93,8 @@ const AzureProvisionerSettings: React.FC<Props> = (props) => {
           message={errorDetails !== "" ? errorMessage + " (" + errorDetails + ")" : errorMessage}
           ctaText={
             errorMessage !== DEFAULT_ERROR_MESSAGE
-                ? "Troubleshooting steps"
-                : null
+              ? "Troubleshooting steps"
+              : null
           }
           errorModalContents={errorMessageToModal(errorMessage)}
         />
@@ -429,119 +431,119 @@ const StyledForm = styled.div`
 const DEFAULT_ERROR_MESSAGE =
   "An error occurred while provisioning your infrastructure. Please try again.";
 const AZURE_CORE_QUOTA_ERROR_MESSAGE =
-    "Your Azure subscription has reached a vCPU core quota in the location";
+  "Your Azure subscription has reached a vCPU core quota in the location";
 const AZURE_MISSING_RESOURCE_PROVIDER_MESSAGE =
-    "Your Azure subscription is missing required resource providers";
+  "Your Azure subscription is missing required resource providers";
 
 const errorMessageToModal = (errorMessage: string) => {
   switch (errorMessage) {
     case AZURE_CORE_QUOTA_ERROR_MESSAGE:
       return (
-          <>
-            <Text size={16} weight={500}>
-              Requesting more cores
-            </Text>
-            <Spacer y={1} />
-            <Text color="helper">
-              You will need to request a quota increase for vCPUs in your region.
-            </Text>
-            <Spacer y={1} />
-            <Step number={1}>
-              Log into
-              <Spacer inline width="5px" />
-              <Link
-                  to="https://login.microsoftonline.com/"
-                  target="_blank"
-              >
-                your Azure account
-              </Link>
-              .
-            </Step>
-            <Spacer y={1} />
-            <Step number={2}>
-              Navigate to
-              <Spacer inline width="5px" />
-              <Link
-                  to="https://portal.azure.com/#view/Microsoft_Azure_Billing/SubscriptionsBlade"
-                  target="_blank"
-              >
-                the Subscriptions page
-              </Link>
-              <Spacer inline width="5px" />
-              and select the subscription you are using to provision Porter.
-            </Step>
-            <Spacer y={1} />
-            <Step number={3}>
-              Select "Usage + Quotas" under "Settings" from the left panel.
-            </Step>
-            <Spacer y={1} />
-            <Step number={4}>
-              Select "Compute" and search for the quotas that have reached usage limits in your region. Request an increase by clicking the pencil icon on the far right.
-            </Step>
-            <Spacer y={1} />
-            <Text color="helper">
-              We recommend an initial quota of 30 vCPUs for both Total Regional Cores and Standard Av2 Family.
-            </Text>
-            <Spacer y={1} />
-            <Step number={5}>
-              Once the request has been approved, return to Porter and retry the
-              provision.
-            </Step>
-            <Spacer y={1} />
-            <Text color="helper">
-              Quota increases can take several minutes to process. If Azure is unable to automatically increase the quota, create a support request as prompted by Azure. Requests are usually fulfilled in a few hours.
-            </Text>
-          </>
+        <>
+          <Text size={16} weight={500}>
+            Requesting more cores
+          </Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            You will need to request a quota increase for vCPUs in your region.
+          </Text>
+          <Spacer y={1} />
+          <Step number={1}>
+            Log into
+            <Spacer inline width="5px" />
+            <Link
+              to="https://login.microsoftonline.com/"
+              target="_blank"
+            >
+              your Azure account
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={2}>
+            Navigate to
+            <Spacer inline width="5px" />
+            <Link
+              to="https://portal.azure.com/#view/Microsoft_Azure_Billing/SubscriptionsBlade"
+              target="_blank"
+            >
+              the Subscriptions page
+            </Link>
+            <Spacer inline width="5px" />
+            and select the subscription you are using to provision Porter.
+          </Step>
+          <Spacer y={1} />
+          <Step number={3}>
+            Select "Usage + Quotas" under "Settings" from the left panel.
+          </Step>
+          <Spacer y={1} />
+          <Step number={4}>
+            Select "Compute" and search for the quotas that have reached usage limits in your region. Request an increase by clicking the pencil icon on the far right.
+          </Step>
+          <Spacer y={1} />
+          <Text color="helper">
+            We recommend an initial quota of 30 vCPUs for both Total Regional Cores and Standard Av2 Family.
+          </Text>
+          <Spacer y={1} />
+          <Step number={5}>
+            Once the request has been approved, return to Porter and retry the
+            provision.
+          </Step>
+          <Spacer y={1} />
+          <Text color="helper">
+            Quota increases can take several minutes to process. If Azure is unable to automatically increase the quota, create a support request as prompted by Azure. Requests are usually fulfilled in a few hours.
+          </Text>
+        </>
       );
     case AZURE_MISSING_RESOURCE_PROVIDER_MESSAGE:
       return (
-          <>
-            <Text size={16} weight={500}>
-              Registering required resource providers
-            </Text>
-            <Spacer y={1} />
-            <Text color="helper">
-              You will need to register all of the following resource providers to your Azure subscription before provisioning: Capacity, Compute, ContainerRegistry, ContainerService, ManagedIdentity, Network, OperationalInsights, OperationsManagement, ResourceGraph, Resources, Storage
-            </Text>
-            <Spacer y={1} />
-            <Step number={1}>
-              Log into
-              <Spacer inline width="5px" />
-              <Link
-                  to="https://login.microsoftonline.com/"
-                  target="_blank"
-              >
-                your Azure account
-              </Link>
-              .
-            </Step>
-            <Spacer y={1} />
-            <Step number={2}>
-              Navigate to
-              <Spacer inline width="5px" />
-              <Link
-                  to="https://portal.azure.com/#view/Microsoft_Azure_Billing/SubscriptionsBlade"
-                  target="_blank"
-              >
-                the Subscriptions page
-              </Link>
-              <Spacer inline width="5px" />
-               and select the subscription you are using to provision Porter.
-            </Step>
-            <Spacer y={1} />
-            <Step number={3}>
-              Select "Resource Providers" under "Settings" from the left panel.
-            </Step>
-            <Spacer y={1} />
-            <Step number={4}>
-              Search for each required resource provider and select "Register" from the top menu bar if it is not already registered.
-            </Step>
-            <Spacer y={1} />
-            <Step number={5}>
-              After confirming that all providers are registered, return to Porter and retry the
-              provision.
-            </Step>
-          </>
+        <>
+          <Text size={16} weight={500}>
+            Registering required resource providers
+          </Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            You will need to register all of the following resource providers to your Azure subscription before provisioning: Capacity, Compute, ContainerRegistry, ContainerService, ManagedIdentity, Network, OperationalInsights, OperationsManagement, ResourceGraph, Resources, Storage
+          </Text>
+          <Spacer y={1} />
+          <Step number={1}>
+            Log into
+            <Spacer inline width="5px" />
+            <Link
+              to="https://login.microsoftonline.com/"
+              target="_blank"
+            >
+              your Azure account
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={2}>
+            Navigate to
+            <Spacer inline width="5px" />
+            <Link
+              to="https://portal.azure.com/#view/Microsoft_Azure_Billing/SubscriptionsBlade"
+              target="_blank"
+            >
+              the Subscriptions page
+            </Link>
+            <Spacer inline width="5px" />
+            and select the subscription you are using to provision Porter.
+          </Step>
+          <Spacer y={1} />
+          <Step number={3}>
+            Select "Resource Providers" under "Settings" from the left panel.
+          </Step>
+          <Spacer y={1} />
+          <Step number={4}>
+            Search for each required resource provider and select "Register" from the top menu bar if it is not already registered.
+          </Step>
+          <Spacer y={1} />
+          <Step number={5}>
+            After confirming that all providers are registered, return to Porter and retry the
+            provision.
+          </Step>
+        </>
       );
     default:
       return null;

+ 29 - 33
dashboard/src/components/CloudFormationForm.tsx

@@ -33,7 +33,7 @@ const CloudFormationForm: React.FC<Props> = ({
   const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
   const [AWSAccountID, setAWSAccountID] = useState("");
   const [AWSAccountIDInputError, setAWSAccountIDInputError] = useState<string | undefined>(undefined);
-  const [currentStep, setCurrentStep] = useState<number>(0);
+  const [currentStep, setCurrentStep] = useState<number>(1);
 
   const { currentProject } = useContext(Context);
   const markStepStarted = async (
@@ -55,7 +55,9 @@ const CloudFormationForm: React.FC<Props> = ({
       }
   ) => {
     try {
-      await api.updateOnboardingStep("<token>", { step, account_id, cloudformation_url, error_message, login_url, external_id }, {});
+      await api.updateOnboardingStep("<token>", { step, account_id, cloudformation_url, error_message, login_url, external_id }, {
+        project_id: currentProject.id,
+      });
     } catch (err) {
       // console.log(err);
     }
@@ -78,13 +80,13 @@ const CloudFormationForm: React.FC<Props> = ({
     }
     // handle case where user resets the input to empty
     if (accountId.trim().length === 0) {
-      setCurrentStep(0);
+      setCurrentStep(1);
       setAWSAccountIDInputError(undefined);
       return;
     }
     const accountIdInputError = getAccountIdInputError(accountId);
     if (accountIdInputError == null) {
-      setCurrentStep(1);
+      setCurrentStep(2);
       if (!hasSentAWSNotif) {
         setHasSentAWSNotif(true);
         markStepStarted({ step: "aws-account-id-complete", account_id: accountId });
@@ -101,7 +103,7 @@ const CloudFormationForm: React.FC<Props> = ({
         }
       }
     } else {
-      setCurrentStep(0);
+      setCurrentStep(1);
     }
     setAWSAccountIDInputError(accountIdInputError);
   };
@@ -155,9 +157,8 @@ const CloudFormationForm: React.FC<Props> = ({
   };
 
   const directToAWSLoginAndProceedStep = () => {
-    const login_url = `https://${AWSAccountID}.signin.aws.amazon.com/console`;
-    markStepStarted({ step: "aws-login-redirect-success", account_id: AWSAccountID, login_url })
-    setCurrentStep(2);
+    const login_url = `https://signin.aws.amazon.com/console`;
+    markStepStarted({ step: "aws-login-redirect-success", login_url })
     window.open(login_url, "_blank")
   }
 
@@ -180,7 +181,26 @@ const CloudFormationForm: React.FC<Props> = ({
           steps={
             [
               <>
-                <Text size={16}>1. Provide your AWS Account ID.</Text>
+                <Text size={16}>1. Log in to your AWS Account.</Text>
+                <Spacer y={0.25} />
+                <Text color="helper">Return to Porter after successful log-in.</Text>
+                <Spacer y={0.5} />
+                <AWSButtonContainer>
+                  <ButtonImg src={aws} />
+                  <Button
+                    width={"170px"}
+                    onClick={directToAWSLoginAndProceedStep}
+                    color="#1E2631"
+                    withBorder
+                  >
+                    Log in
+                  </Button>
+                </AWSButtonContainer>
+              </>,
+              <>
+                <Text size={16}>2. Provide your AWS Account ID.</Text>
+                <Spacer y={0.25} />
+                <Text color="helper">Make sure this is the ID of the account you are currently logged into, and would like to provision resources in.</Text>
                 <Spacer y={0.5} />
                 <Input
                   label={
@@ -202,30 +222,6 @@ const CloudFormationForm: React.FC<Props> = ({
                   error={AWSAccountIDInputError}
                 />
               </>,
-              <>
-                <Text size={16}>2. Log in to your AWS Account.</Text>
-                <Spacer y={0.25} />
-                <Text color="helper">Return to Porter after successful log-in.</Text>
-                <Spacer y={0.5} />
-                <AWSButtonContainer>
-                  <ButtonImg src={aws} />
-                  <Button
-                    width={"170px"}
-                    onClick={directToAWSLoginAndProceedStep}
-                    color="#1E2631"
-                    withBorder
-                  >
-                    Log in
-                  </Button>
-                </AWSButtonContainer>
-                {/* escape hatch for dev use only */}
-                {process.env.TRUST_ARN != null && process.env.TRUST_ARN !== "arn:aws:iam::108458755588:role/CAPIManagement" &&
-                  <>
-                    <Spacer y={0.5} />
-                    <Link onClick={() => setCurrentStep(4)} hasunderline>Skip this step</Link>
-                  </>
-                }
-              </>,
               <>
                 <Text size={16}>3. Create an AWS Cloudformation Stack.</Text>
                 <Spacer y={0.25} />

+ 3 - 3
dashboard/src/components/ProvisionerFlow.tsx

@@ -17,7 +17,7 @@ const providers = ["aws", "gcp", "azure"];
 
 type Props = {};
 
-const ProvisionerFlow: React.FC<Props> = ({}) => {
+const ProvisionerFlow: React.FC<Props> = ({ }) => {
   const {
     usage,
     hasBillingEnabled,
@@ -40,7 +40,7 @@ const ProvisionerFlow: React.FC<Props> = ({}) => {
 
   const markStepCostConsent = async (step: string, provider: string) => {
     try {
-      await api.updateOnboardingStep("<token>", { step, provider }, {});
+      await api.updateOnboardingStep("<token>", { step, provider }, { project_id: currentProject.id });
     } catch (err) {
       console.log(err);
     }
@@ -85,7 +85,7 @@ const ProvisionerFlow: React.FC<Props> = ({}) => {
                   <Icon src={providerInfo.icon} />
                   <BlockTitle>{providerInfo.label}</BlockTitle>
                   <BlockDescription>
-                      {(provider === "azure" && !currentProject?.azure_enabled) ||
+                    {(provider === "azure" && !currentProject?.azure_enabled) ||
                       provider === "gcp" ? providerInfo.tagline : "Hosted in your own cloud"}
                   </BlockDescription>
                 </Block>

+ 15 - 4
dashboard/src/components/ProvisionerSettings.tsx

@@ -123,9 +123,17 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
   const [isReadOnly, setIsReadOnly] = useState(false);
   const [errorMessage, setErrorMessage] = useState<string>(undefined);
   const [isClicked, setIsClicked] = useState(false);
-  const markStepStarted = async (step: string) => {
+  const markStepStarted = async (step: string, errMessage?: string) => {
     try {
-      await api.updateOnboardingStep("<token>", { step }, {});
+      await api.updateOnboardingStep("<token>", {
+        step,
+        error_message: errMessage,
+        region: awsRegion,
+      },
+        {
+          project_id: currentProject.id,
+        },
+      );
     } catch (err) {
       // console.log(err);
     }
@@ -323,14 +331,16 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
             id: currentProject.id,
           }
         );
-
-        markStepStarted("provisioning-started");
       }
 
       const res = await api.createContract("<token>", data, {
         project_id: currentProject.id,
       });
 
+      if (!props.clusterId) {
+        markStepStarted("provisioning-started");
+      }
+
       // Only refresh and set clusters on initial create
       // if (!props.clusterId) {
       setShouldRefreshClusters(true);
@@ -370,6 +380,7 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
       } else {
         setErrorMessage(DEFAULT_ERROR_MESSAGE);
       }
+      markStepStarted("provisioning-failed", errMessage);
     } finally {
       setIsReadOnly(false);
       setIsClicked(false);

+ 6 - 3
dashboard/src/shared/api.tsx

@@ -2453,10 +2453,13 @@ const updateOnboardingStep = baseApi<
     error_message?: string;
     login_url?: string;
     external_id?: string;
+    region?: string;
   },
-  {}
->("POST", (pathParams) => {
-  return `/api/onboarding_step`;
+  {
+    project_id: number;
+  }
+>("POST", ({ project_id }) => {
+  return `/api//projects/${project_id}/onboarding_step`;
 });
 
 const updateStackStep = baseApi<

+ 2 - 0
internal/analytics/track_events.go

@@ -18,6 +18,7 @@ const (
 	AWSCreateIntegrationSuccess SegmentEvent = "AWS Create Integration Success"
 	AWSCreateIntegrationFailure SegmentEvent = "AWS Create Integration Failure"
 	ProvisioningAttempted       SegmentEvent = "Provisioning Attempted"
+	ProvisioningFailure         SegmentEvent = "Provisioning Failure"
 
 	ClusterProvisioningStart   SegmentEvent = "Cluster Provisioning Started"
 	ClusterProvisioningError   SegmentEvent = "Cluster Provisioning Error"
@@ -52,4 +53,5 @@ const (
 	StackLaunchSuccess  SegmentEvent = "Stack Launch Success"
 	StackLaunchFailure  SegmentEvent = "Stack Launch Failure"
 	StackDeletion       SegmentEvent = "Stack Deletion"
+	StackBuildFailure   SegmentEvent = "Stack Build Failure"
 )

+ 57 - 21
internal/analytics/tracks.go

@@ -182,7 +182,7 @@ func CostConsentCompletedTrack(opts *CostConsentCompletedTrackOpts) segmentTrack
 
 // AWSInputTrackOpts are the options for creating a track when a user inputs a complete AWS account ID
 type AWSInputTrackOpts struct {
-	*UserScopedTrackOpts
+	*ProjectScopedTrackOpts
 
 	Email       string
 	FirstName   string
@@ -199,14 +199,14 @@ func AWSInputTrack(opts *AWSInputTrackOpts) segmentTrack {
 	additionalProps["company"] = opts.CompanyName
 	additionalProps["account_id"] = opts.AccountId
 
-	return getSegmentUserTrack(
-		opts.UserScopedTrackOpts,
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
 		getDefaultSegmentTrack(additionalProps, AWSInputted),
 	)
 }
 
 type AWSRedirectOpts struct {
-	*UserScopedTrackOpts
+	*ProjectScopedTrackOpts
 
 	Email             string
 	FirstName         string
@@ -228,8 +228,8 @@ func AWSCloudformationRedirectSuccess(opts *AWSRedirectOpts) segmentTrack {
 	additionalProps["cloudformation_url"] = opts.CloudformationURL
 	additionalProps["external_id"] = opts.ExternalId
 
-	return getSegmentUserTrack(
-		opts.UserScopedTrackOpts,
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
 		getDefaultSegmentTrack(additionalProps, AWSCloudformationRedirect),
 	)
 }
@@ -250,7 +250,7 @@ func AWSLoginRedirectSuccess(opts *AWSRedirectOpts) segmentTrack {
 }
 
 type AWSCreateIntegrationOpts struct {
-	*UserScopedTrackOpts
+	*ProjectScopedTrackOpts
 
 	Email        string
 	FirstName    string
@@ -269,8 +269,8 @@ func AWSCreateIntegrationSucceeded(opts *AWSCreateIntegrationOpts) segmentTrack
 	additionalProps["company"] = opts.CompanyName
 	additionalProps["account_id"] = opts.AccountId
 
-	return getSegmentUserTrack(
-		opts.UserScopedTrackOpts,
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
 		getDefaultSegmentTrack(additionalProps, AWSCreateIntegrationSuccess),
 	)
 }
@@ -285,8 +285,8 @@ func AWSCreateIntegrationFailed(opts *AWSCreateIntegrationOpts) segmentTrack {
 	additionalProps["error_message"] = opts.ErrorMessage
 	additionalProps["external_id"] = opts.ExternalId
 
-	return getSegmentUserTrack(
-		opts.UserScopedTrackOpts,
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
 		getDefaultSegmentTrack(additionalProps, AWSCreateIntegrationFailure),
 	)
 }
@@ -308,7 +308,7 @@ func CredentialStepTrack(opts *CredentialStepTrackOpts) segmentTrack {
 
 // PreProvisionCheckTrackOpts are the options for creating a track when a user checks if they can provision
 type PreProvisionCheckTrackOpts struct {
-	*UserScopedTrackOpts
+	*ProjectScopedTrackOpts
 
 	Email       string
 	FirstName   string
@@ -323,20 +323,22 @@ func PreProvisionCheckTrack(opts *PreProvisionCheckTrackOpts) segmentTrack {
 	additionalProps["name"] = opts.FirstName + " " + opts.LastName
 	additionalProps["company"] = opts.CompanyName
 
-	return getSegmentUserTrack(
-		opts.UserScopedTrackOpts,
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
 		getDefaultSegmentTrack(additionalProps, PreProvisionCheck),
 	)
 }
 
 // ProvisioningAttemptedTrackOpts are the options for creating a track when a user attempts provisioning
 type ProvisioningAttemptTrackOpts struct {
-	*UserScopedTrackOpts
+	*ProjectScopedTrackOpts
 
-	Email       string
-	FirstName   string
-	LastName    string
-	CompanyName string
+	Email        string
+	FirstName    string
+	LastName     string
+	CompanyName  string
+	ErrorMessage string
+	Region       string
 }
 
 // ProvisioningAttemptTrack returns a track for when a user attempts provisioning
@@ -345,13 +347,29 @@ func ProvisioningAttemptTrack(opts *ProvisioningAttemptTrackOpts) segmentTrack {
 	additionalProps["email"] = opts.Email
 	additionalProps["name"] = opts.FirstName + " " + opts.LastName
 	additionalProps["company"] = opts.CompanyName
+	additionalProps["region"] = opts.Region
 
-	return getSegmentUserTrack(
-		opts.UserScopedTrackOpts,
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
 		getDefaultSegmentTrack(additionalProps, ProvisioningAttempted),
 	)
 }
 
+// PreProvisionCheckTrack returns a track for when a user attempts provisioning
+func ProvisionFailureTrack(opts *ProvisioningAttemptTrackOpts) 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
+	additionalProps["region"] = opts.Region
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, ProvisioningFailure),
+	)
+}
+
 // ClusterProvisioningStartTrackOpts are the options for creating a track when a cluster
 // has started provisioning
 type ClusterProvisioningStartTrackOpts struct {
@@ -836,3 +854,21 @@ func StackDeletionTrack(opts *StackDeletionOpts) segmentTrack {
 		getDefaultSegmentTrack(additionalProps, StackDeletion),
 	)
 }
+
+// StackBuildFailureOpts are the options for creating a track when a stack fails to build
+type StackBuildFailureOpts struct {
+	*ProjectScopedTrackOpts
+
+	StackName string
+}
+
+// StackBuildFailureTrack returns a track for when a stack fails to build
+func StackBuildFailureTrack(opts *StackBuildFailureOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["stack_name"] = opts.StackName
+
+	return getSegmentProjectTrack(
+		opts.ProjectScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, StackBuildFailure),
+	)
+}