Bläddra i källkod

Adding more analytics to cloudformation step (#3247)

Feroze Mohideen 2 år sedan
förälder
incheckning
81afead682

+ 18 - 7
api/server/handlers/project_integration/create_aws.go

@@ -1,7 +1,6 @@
 package project_integration
 
 import (
-	"fmt"
 	"net/http"
 
 	"github.com/bufbuild/connect-go"
@@ -13,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/telemetry"
 )
 
 type CreateAWSHandler struct {
@@ -30,12 +30,16 @@ func NewCreateAWSHandler(
 }
 
 func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-	ctx := r.Context()
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-aws-integration")
+	defer span.End()
+
+	user, _ := ctx.Value(types.UserScope).(*models.User)
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
 	request := &types.CreateAWSRequest{}
 	if ok := p.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
 
@@ -43,7 +47,8 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	aws, err := p.Repo().AWSIntegration().CreateAWSIntegration(aws)
 	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		err = telemetry.Error(ctx, span, err, "error creating aws integration")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
@@ -60,13 +65,19 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			TargetArn:       request.TargetArn,
 			ExternalId:      request.ExternalID,
 		}
+		telemetry.WithAttributes(span,
+			telemetry.AttributeKV{Key: "target-arn", Value: request.TargetArn},
+			telemetry.AttributeKV{Key: "external-id", Value: request.ExternalID},
+			telemetry.AttributeKV{Key: "target-access-id", Value: request.AWSAccessKeyID},
+		)
 		credResp, err := p.Config().ClusterControlPlaneClient.CreateAssumeRoleChain(ctx, connect.NewRequest(&credReq))
 		if err != nil {
-			e := fmt.Errorf("unable to create CAPI required credential: %w", err)
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusPreconditionFailed, err.Error()))
+			err = telemetry.Error(ctx, span, err, "error creating CAPI required credential")
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusPreconditionFailed, err.Error()))
 			return
 		}
 		res.CloudProviderCredentialIdentifier = credResp.Msg.TargetArn
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cloud-provider-credential-identifier", Value: credResp.Msg.TargetArn})
 	}
 
 	p.WriteResult(w, r, res)

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

@@ -62,6 +62,42 @@ func (v *UpdateOnboardingStepHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 			FirstName:           user.FirstName,
 			LastName:            user.LastName,
 			CompanyName:         user.CompanyName,
+			AccountId:           request.AccountId,
+		}))
+	}
+
+	if request.Step == "aws-cloudformation-redirect-success" {
+		v.Config().AnalyticsClient.Track(analytics.AWSCloudformationRedirectSuccess(&analytics.AWSCloudFormationRedirectOpts{
+			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
+			Email:               user.Email,
+			FirstName:           user.FirstName,
+			LastName:            user.LastName,
+			CompanyName:         user.CompanyName,
+			AccountId:           request.AccountId,
+			CloudformationURL:   request.CloudformationURL,
+		}))
+	}
+
+	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,
+		}))
+	}
+
+	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,
 		}))
 	}
 

+ 5 - 2
api/types/user.go

@@ -82,6 +82,9 @@ type UpdateUserInfoRequest struct {
 }
 
 type UpdateOnboardingStepRequest struct {
-	Step     string `json:"step" form:"required,max=255"`
-	Provider string `json:"provider"`
+	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"`
 }

+ 31 - 10
dashboard/src/components/CloudFormationForm.tsx

@@ -34,12 +34,25 @@ const CloudFormationForm: React.FC<Props> = ({
   const [hasSentAWSNotif, setHasSentAWSNotif] = useState(false);
   const [grantPermissionsError, setGrantPermissionsError] = useState("");
   const [roleStatus, setRoleStatus] = useState("");
-  const [errorMessage, setErrorMessage] = useState(undefined);
+  const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
   const [AWSAccountID, setAWSAccountID] = useState("");
   const { currentProject } = useContext(Context);
-  const markStepStarted = async (step: string) => {
+  const markStepStarted = async (
+    {
+      step,
+      account_id = "",
+      cloudformation_url = "",
+      error_message = "",
+    }:
+      {
+        step: string;
+        account_id?: string
+        cloudformation_url?: string
+        error_message?: string
+      }
+  ) => {
     try {
-      await api.updateOnboardingStep("<token>", { step }, {});
+      await api.updateOnboardingStep("<token>", { step, account_id, cloudformation_url, error_message }, {});
     } catch (err) {
       // console.log(err);
     }
@@ -47,7 +60,6 @@ const CloudFormationForm: React.FC<Props> = ({
 
   const getExternalId = () => {
     let externalId = localStorage.getItem(AWSAccountID)
-    console.log(externalId)
     if (!externalId) {
       externalId = uuidv4()
       localStorage.setItem(AWSAccountID, externalId);
@@ -63,6 +75,10 @@ const CloudFormationForm: React.FC<Props> = ({
     setRoleStatus("loading");
     setErrorMessage(undefined)
     try {
+      if (currentProject == null) {
+        setErrorMessage("Could not find current project.")
+        return;
+      };
       await api
         .createAWSIntegration(
           "<token>",
@@ -75,21 +91,26 @@ const CloudFormationForm: React.FC<Props> = ({
           }
         );
       setRoleStatus("successful")
+      markStepStarted({ step: "aws-create-integration-success", account_id: AWSAccountID })
       proceed(targetARN);
     } catch (err) {
-      console.log(err);
       setRoleStatus("");
       setErrorMessage("Porter could not access your AWS account. Please make sure you have granted permissions and try again.")
+      markStepStarted({
+        step: "aws-create-integration-failure",
+        account_id: AWSAccountID,
+        error_message: err?.response?.data?.error ??
+          err?.toString() ?? "unable to determine error - check honeycomb"
+      })
     }
   };
 
   const directToCloudFormation = () => {
     let externalId = getExternalId();
     let trustArn = process.env.TRUST_ARN ? process.env.TRUST_ARN : "arn:aws:iam::108458755588:role/CAPIManagement";
-    window.open(
-      `https://console.aws.amazon.com/cloudformation/home?
-      #/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-policy.json&stackName=PorterRole&param_ExternalIdParameter=${externalId}&param_TrustArnParameter=${trustArn}`
-    )
+    const cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-policy.json&stackName=PorterRole&param_ExternalIdParameter=${externalId}&param_TrustArnParameter=${trustArn}`
+    markStepStarted({ step: "aws-cloudformation-redirect-success", account_id: AWSAccountID, cloudformation_url })
+    window.open(cloudformation_url, "_blank")
   }
 
   const renderContent = () => {
@@ -126,7 +147,7 @@ const CloudFormationForm: React.FC<Props> = ({
               }
               if (e.trim().length === 12 && !hasSentAWSNotif) {
                 setHasSentAWSNotif(true);
-                markStepStarted("aws-account-id-complete");
+                markStepStarted({ step: "aws-account-id-complete", account_id: e.trim() });
               }
               setGrantPermissionsError("");
               setAWSAccountID(e.trim());

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

@@ -2448,6 +2448,9 @@ const updateOnboardingStep = baseApi<
   {
     step: string;
     provider?: string;
+    account_id?: string;
+    cloudformation_url?: string;
+    error_message?: string;
   },
   {}
 >("POST", (pathParams) => {

+ 10 - 7
internal/analytics/track_events.go

@@ -8,12 +8,15 @@ const (
 	UserVerifyEmail SegmentEvent = "User Verified Email"
 	ProjectCreate   SegmentEvent = "New Project Event"
 
-	CostConsentOpened      SegmentEvent = "Cost Consent Opened"
-	CostConsentComplete    SegmentEvent = "Cost Consent Complete"
-	CredentialStepComplete SegmentEvent = "Credential Step Complete"
-	PreProvisionCheck      SegmentEvent = "Pre Provision Check Started"
-	AWSInputted            SegmentEvent = "AWS Account ID Inputted"
-	ProvisioningAttempted  SegmentEvent = "Provisioning Attempted"
+	CostConsentOpened           SegmentEvent = "Cost Consent Opened"
+	CostConsentComplete         SegmentEvent = "Cost Consent Complete"
+	CredentialStepComplete      SegmentEvent = "Credential Step Complete"
+	PreProvisionCheck           SegmentEvent = "Pre Provision Check Started"
+	AWSInputted                 SegmentEvent = "AWS Account ID Inputted"
+	AWSCloudformationRedirect   SegmentEvent = "AWS Cloudformation Redirect"
+	AWSCreateIntegrationSuccess SegmentEvent = "AWS Create Integration Success"
+	AWSCreateIntegrationFailure SegmentEvent = "AWS Create Integration Failure"
+	ProvisioningAttempted       SegmentEvent = "Provisioning Attempted"
 
 	ClusterProvisioningStart   SegmentEvent = "Cluster Provisioning Started"
 	ClusterProvisioningError   SegmentEvent = "Cluster Provisioning Error"
@@ -42,7 +45,7 @@ const (
 	ClusterDestroyingStart   SegmentEvent = "Cluster Destroying Start"
 	ClusterDestroyingSuccess SegmentEvent = "Cluster Destroying Success"
 
-	// stacks
+	// porter apps
 	StackLaunchStart    SegmentEvent = "Stack Launch Started"
 	StackLaunchComplete SegmentEvent = "Stack Launch Complete"
 	StackLaunchSuccess  SegmentEvent = "Stack Launch Success"

+ 68 - 0
internal/analytics/tracks.go

@@ -188,6 +188,7 @@ type AWSInputTrackOpts struct {
 	FirstName   string
 	LastName    string
 	CompanyName string
+	AccountId   string
 }
 
 // AWSInputTrack returns a track for when a user inputs a complete AWS account ID
@@ -196,6 +197,7 @@ func AWSInputTrack(opts *AWSInputTrackOpts) segmentTrack {
 	additionalProps["email"] = opts.Email
 	additionalProps["name"] = opts.FirstName + " " + opts.LastName
 	additionalProps["company"] = opts.CompanyName
+	additionalProps["account_id"] = opts.AccountId
 
 	return getSegmentUserTrack(
 		opts.UserScopedTrackOpts,
@@ -203,6 +205,72 @@ func AWSInputTrack(opts *AWSInputTrackOpts) segmentTrack {
 	)
 }
 
+type AWSCloudFormationRedirectOpts struct {
+	*UserScopedTrackOpts
+
+	Email             string
+	FirstName         string
+	LastName          string
+	CompanyName       string
+	AccountId         string
+	CloudformationURL string
+}
+
+// AWSCloudformationRedirectSuccess returns a track for when a user clicks 'grant permissions' and gets redirected to cloudformation
+func AWSCloudformationRedirectSuccess(opts *AWSCloudFormationRedirectOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["email"] = opts.Email
+	additionalProps["name"] = opts.FirstName + " " + opts.LastName
+	additionalProps["company"] = opts.CompanyName
+	additionalProps["account_id"] = opts.AccountId
+	additionalProps["cloudformation_url"] = opts.CloudformationURL
+
+	return getSegmentUserTrack(
+		opts.UserScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, AWSCloudformationRedirect),
+	)
+}
+
+type AWSCreateIntegrationOpts struct {
+	*UserScopedTrackOpts
+
+	Email        string
+	FirstName    string
+	LastName     string
+	CompanyName  string
+	AccountId    string
+	ErrorMessage string
+}
+
+// AWSCreateIntegrationSucceeded returns a track for when a user succeeds in creating an aws integration
+func AWSCreateIntegrationSucceeded(opts *AWSCreateIntegrationOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["email"] = opts.Email
+	additionalProps["name"] = opts.FirstName + " " + opts.LastName
+	additionalProps["company"] = opts.CompanyName
+	additionalProps["account_id"] = opts.AccountId
+
+	return getSegmentUserTrack(
+		opts.UserScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, AWSCreateIntegrationSuccess),
+	)
+}
+
+// AWSCreateIntegrationSucceeded returns a track for when a user succeeds in creating an aws integration
+func AWSCreateIntegrationFailed(opts *AWSCreateIntegrationOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["email"] = opts.Email
+	additionalProps["name"] = opts.FirstName + " " + opts.LastName
+	additionalProps["company"] = opts.CompanyName
+	additionalProps["account_id"] = opts.AccountId
+	additionalProps["error_message"] = opts.ErrorMessage
+
+	return getSegmentUserTrack(
+		opts.UserScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, AWSCreateIntegrationFailure),
+	)
+}
+
 // CredentialStepTrackOpts are the options for creating a track when a user completes the credential step
 type CredentialStepTrackOpts struct {
 	*UserScopedTrackOpts