瀏覽代碼

Adding more analytics to cloudformation step (#3247)

Feroze Mohideen 2 年之前
父節點
當前提交
81afead682

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

@@ -1,7 +1,6 @@
 package project_integration
 package project_integration
 
 
 import (
 import (
-	"fmt"
 	"net/http"
 	"net/http"
 
 
 	"github.com/bufbuild/connect-go"
 	"github.com/bufbuild/connect-go"
@@ -13,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/telemetry"
 )
 )
 
 
 type CreateAWSHandler struct {
 type CreateAWSHandler struct {
@@ -30,12 +30,16 @@ func NewCreateAWSHandler(
 }
 }
 
 
 func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 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{}
 	request := &types.CreateAWSRequest{}
 	if ok := p.DecodeAndValidate(w, r, request); !ok {
 	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
 		return
 	}
 	}
 
 
@@ -43,7 +47,8 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 
 	aws, err := p.Repo().AWSIntegration().CreateAWSIntegration(aws)
 	aws, err := p.Repo().AWSIntegration().CreateAWSIntegration(aws)
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
@@ -60,13 +65,19 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			TargetArn:       request.TargetArn,
 			TargetArn:       request.TargetArn,
 			ExternalId:      request.ExternalID,
 			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))
 		credResp, err := p.Config().ClusterControlPlaneClient.CreateAssumeRoleChain(ctx, connect.NewRequest(&credReq))
 		if err != nil {
 		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
 			return
 		}
 		}
 		res.CloudProviderCredentialIdentifier = credResp.Msg.TargetArn
 		res.CloudProviderCredentialIdentifier = credResp.Msg.TargetArn
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cloud-provider-credential-identifier", Value: credResp.Msg.TargetArn})
 	}
 	}
 
 
 	p.WriteResult(w, r, res)
 	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,
 			FirstName:           user.FirstName,
 			LastName:            user.LastName,
 			LastName:            user.LastName,
 			CompanyName:         user.CompanyName,
 			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 {
 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 [hasSentAWSNotif, setHasSentAWSNotif] = useState(false);
   const [grantPermissionsError, setGrantPermissionsError] = useState("");
   const [grantPermissionsError, setGrantPermissionsError] = useState("");
   const [roleStatus, setRoleStatus] = useState("");
   const [roleStatus, setRoleStatus] = useState("");
-  const [errorMessage, setErrorMessage] = useState(undefined);
+  const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
   const [AWSAccountID, setAWSAccountID] = useState("");
   const [AWSAccountID, setAWSAccountID] = useState("");
   const { currentProject } = useContext(Context);
   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 {
     try {
-      await api.updateOnboardingStep("<token>", { step }, {});
+      await api.updateOnboardingStep("<token>", { step, account_id, cloudformation_url, error_message }, {});
     } catch (err) {
     } catch (err) {
       // console.log(err);
       // console.log(err);
     }
     }
@@ -47,7 +60,6 @@ const CloudFormationForm: React.FC<Props> = ({
 
 
   const getExternalId = () => {
   const getExternalId = () => {
     let externalId = localStorage.getItem(AWSAccountID)
     let externalId = localStorage.getItem(AWSAccountID)
-    console.log(externalId)
     if (!externalId) {
     if (!externalId) {
       externalId = uuidv4()
       externalId = uuidv4()
       localStorage.setItem(AWSAccountID, externalId);
       localStorage.setItem(AWSAccountID, externalId);
@@ -63,6 +75,10 @@ const CloudFormationForm: React.FC<Props> = ({
     setRoleStatus("loading");
     setRoleStatus("loading");
     setErrorMessage(undefined)
     setErrorMessage(undefined)
     try {
     try {
+      if (currentProject == null) {
+        setErrorMessage("Could not find current project.")
+        return;
+      };
       await api
       await api
         .createAWSIntegration(
         .createAWSIntegration(
           "<token>",
           "<token>",
@@ -75,21 +91,26 @@ const CloudFormationForm: React.FC<Props> = ({
           }
           }
         );
         );
       setRoleStatus("successful")
       setRoleStatus("successful")
+      markStepStarted({ step: "aws-create-integration-success", account_id: AWSAccountID })
       proceed(targetARN);
       proceed(targetARN);
     } catch (err) {
     } catch (err) {
-      console.log(err);
       setRoleStatus("");
       setRoleStatus("");
       setErrorMessage("Porter could not access your AWS account. Please make sure you have granted permissions and try again.")
       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 = () => {
   const directToCloudFormation = () => {
     let externalId = getExternalId();
     let externalId = getExternalId();
     let trustArn = process.env.TRUST_ARN ? process.env.TRUST_ARN : "arn:aws:iam::108458755588:role/CAPIManagement";
     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 = () => {
   const renderContent = () => {
@@ -126,7 +147,7 @@ const CloudFormationForm: React.FC<Props> = ({
               }
               }
               if (e.trim().length === 12 && !hasSentAWSNotif) {
               if (e.trim().length === 12 && !hasSentAWSNotif) {
                 setHasSentAWSNotif(true);
                 setHasSentAWSNotif(true);
-                markStepStarted("aws-account-id-complete");
+                markStepStarted({ step: "aws-account-id-complete", account_id: e.trim() });
               }
               }
               setGrantPermissionsError("");
               setGrantPermissionsError("");
               setAWSAccountID(e.trim());
               setAWSAccountID(e.trim());

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

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

+ 10 - 7
internal/analytics/track_events.go

@@ -8,12 +8,15 @@ const (
 	UserVerifyEmail SegmentEvent = "User Verified Email"
 	UserVerifyEmail SegmentEvent = "User Verified Email"
 	ProjectCreate   SegmentEvent = "New Project Event"
 	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"
 	ClusterProvisioningStart   SegmentEvent = "Cluster Provisioning Started"
 	ClusterProvisioningError   SegmentEvent = "Cluster Provisioning Error"
 	ClusterProvisioningError   SegmentEvent = "Cluster Provisioning Error"
@@ -42,7 +45,7 @@ const (
 	ClusterDestroyingStart   SegmentEvent = "Cluster Destroying Start"
 	ClusterDestroyingStart   SegmentEvent = "Cluster Destroying Start"
 	ClusterDestroyingSuccess SegmentEvent = "Cluster Destroying Success"
 	ClusterDestroyingSuccess SegmentEvent = "Cluster Destroying Success"
 
 
-	// stacks
+	// porter apps
 	StackLaunchStart    SegmentEvent = "Stack Launch Started"
 	StackLaunchStart    SegmentEvent = "Stack Launch Started"
 	StackLaunchComplete SegmentEvent = "Stack Launch Complete"
 	StackLaunchComplete SegmentEvent = "Stack Launch Complete"
 	StackLaunchSuccess  SegmentEvent = "Stack Launch Success"
 	StackLaunchSuccess  SegmentEvent = "Stack Launch Success"

+ 68 - 0
internal/analytics/tracks.go

@@ -188,6 +188,7 @@ type AWSInputTrackOpts struct {
 	FirstName   string
 	FirstName   string
 	LastName    string
 	LastName    string
 	CompanyName string
 	CompanyName string
+	AccountId   string
 }
 }
 
 
 // AWSInputTrack returns a track for when a user inputs a complete AWS account ID
 // 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["email"] = opts.Email
 	additionalProps["name"] = opts.FirstName + " " + opts.LastName
 	additionalProps["name"] = opts.FirstName + " " + opts.LastName
 	additionalProps["company"] = opts.CompanyName
 	additionalProps["company"] = opts.CompanyName
+	additionalProps["account_id"] = opts.AccountId
 
 
 	return getSegmentUserTrack(
 	return getSegmentUserTrack(
 		opts.UserScopedTrackOpts,
 		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
 // CredentialStepTrackOpts are the options for creating a track when a user completes the credential step
 type CredentialStepTrackOpts struct {
 type CredentialStepTrackOpts struct {
 	*UserScopedTrackOpts
 	*UserScopedTrackOpts