diff --git a/Makefile b/Makefile index 8ed42ad8a..8bafa8d82 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ BUILD ?= $(shell git rev-parse --short HEAD) DKR_HOST ?= dkr.plural.sh GOOS ?= darwin GOARCH ?= amd64 -BASE_LDFLAGS ?= -X main.version=$(APP_VSN) -X main.commit=$(BUILD) -X main.date=$(APP_DATE) -X github.com/pluralsh/plural/pkg/scm.GitlabClientSecret=${GITLAB_CLIENT_SECRET} -X github.com/pluralsh/plural/pkg/scm.BitbucketClientSecret=${BITBUCKET_CLIENT_SECRET} +BASE_LDFLAGS ?= -X github.com/pluralsh/plural/pkg/utils.version=$(APP_VSN) -X main.version=$(APP_VSN) -X main.commit=$(BUILD) -X main.date=$(APP_DATE) -X github.com/pluralsh/plural/pkg/scm.GitlabClientSecret=${GITLAB_CLIENT_SECRET} -X github.com/pluralsh/plural/pkg/scm.BitbucketClientSecret=${BITBUCKET_CLIENT_SECRET} OUTFILE ?= plural.o help: diff --git a/cmd/plural/config_test.go b/cmd/plural/config_test.go index e6b04745b..ce6c34777 100644 --- a/cmd/plural/config_test.go +++ b/cmd/plural/config_test.go @@ -24,13 +24,13 @@ func TestPluralConfigCommand(t *testing.T) { { name: `test "config read" command when config file doesn't exists'`, args: []string{plural.ApplicationName, "config", "read"}, - expectedResponse: "apiVersion: platform.plural.sh/v1alpha1\nkind: Config\nmetadata: null\nspec:\n email: \"\"\n token: \"\"\n namespacePrefix: \"\"\n endpoint: \"\"\n lockProfile: \"\"\n reportErrors: false\n", + expectedResponse: "apiVersion: platform.plural.sh/v1alpha1\nkind: Config\nmetadata: null\nspec:\n email: \"\"\n id: \"\"\n token: \"\"\n namespacePrefix: \"\"\n endpoint: \"\"\n lockProfile: \"\"\n reportErrors: false\n", }, { name: `test "config read" command with default test config'`, args: []string{plural.ApplicationName, "config", "read"}, createConfig: true, - expectedResponse: "apiVersion: platform.plural.sh/v1alpha1\nkind: Config\nmetadata: null\nspec:\n email: test@plural.sh\n token: abc\n namespacePrefix: test\n endpoint: http://example.com\n lockProfile: abc\n reportErrors: false\n", + expectedResponse: "apiVersion: platform.plural.sh/v1alpha1\nkind: Config\nmetadata: null\nspec:\n email: test@plural.sh\n id: \"\"\n token: abc\n namespacePrefix: test\n endpoint: http://example.com\n lockProfile: abc\n reportErrors: false\n", }, } for _, test := range tests { diff --git a/cmd/plural/deploy.go b/cmd/plural/deploy.go index 35392de0d..17ed30e5a 100644 --- a/cmd/plural/deploy.go +++ b/cmd/plural/deploy.go @@ -237,8 +237,11 @@ func (p *Plural) deploy(c *cli.Context) error { if c.Bool("silence") { continue } - - if man, err := fetchManifest(repo); err == nil && man.Wait { + man, err := fetchManifest(repo) + if err != nil { + return err + } + if man.Wait { if kubeConf, err := kubernetes.KubeConfig(); err == nil { fmt.Printf("Waiting for %s to become ready...\n", repo) if err := application.SilentWait(kubeConf, repo); err != nil { @@ -247,7 +250,25 @@ func (p *Plural) deploy(c *cli.Context) error { fmt.Println("") } } - + for _, ch := range man.Charts { + utils.PosthogCapture(utils.DeployPosthogEvent, utils.PosthogProperties{ + ApplicationName: installation.Repository.Name, + ApplicationID: installation.Id, + PackageType: "helm", + PackageName: ch.Name, + PackageId: ch.Id, + PackageVersion: ch.Version, + }) + } + for _, tf := range man.Terraform { + utils.PosthogCapture(utils.DeployPosthogEvent, utils.PosthogProperties{ + ApplicationName: installation.Repository.Name, + ApplicationID: installation.Id, + PackageType: "terraform", + PackageName: tf.Name, + PackageId: tf.Id, + }) + } if err := scaffold.Notes(installation); err != nil { return err } diff --git a/cmd/plural/init.go b/cmd/plural/init.go index d9d761502..59a7c0ac5 100644 --- a/cmd/plural/init.go +++ b/cmd/plural/init.go @@ -6,13 +6,14 @@ import ( "path/filepath" "time" + "github.com/pluralsh/plural/pkg/manifest" + "github.com/pkg/browser" "github.com/urfave/cli" "github.com/pluralsh/plural/pkg/api" "github.com/pluralsh/plural/pkg/config" "github.com/pluralsh/plural/pkg/crypto" - "github.com/pluralsh/plural/pkg/manifest" "github.com/pluralsh/plural/pkg/provider" "github.com/pluralsh/plural/pkg/scm" "github.com/pluralsh/plural/pkg/server" @@ -85,6 +86,16 @@ func (p *Plural) handleInit(c *cli.Context) error { return err } + project, err := manifest.FetchProject() + if err != nil { + return err + } + project.Owner.ID = me.Id + project.SendMetrics = affirm("Would you be willing to send installation metrics to Plural? We will store data in an aggregated form to analyze the application's deployment", "PLURAL_INIT_AFFIRM_SEND_METRICS") + if err := project.Write(manifest.ProjectManifestPath()); err != nil { + return err + } + utils.Success("Workspace is properly configured!\n") if gitCreated { utils.Highlight("Be sure to `cd %s` to use your configured git repo\n", repo) @@ -164,6 +175,7 @@ func postLogin(conf *config.Config, client api.Client, c *cli.Context) error { } conf.Email = me.Email + conf.ID = me.Id fmt.Printf("\nlogged in as %s!\n", me.Email) saEmail := c.String("service-account") diff --git a/go.mod b/go.mod index f8670ef6a..58d43b179 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/pluralsh/gqlclient v1.3.10 github.com/pluralsh/plural-operator v0.5.3 github.com/pluralsh/polly v0.0.6 + github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a github.com/rodaine/hclencoder v0.0.1 github.com/samber/lo v1.33.0 github.com/thoas/go-funk v0.9.2 @@ -104,6 +105,7 @@ require ( github.com/pluralsh/controller-reconcile-helper v0.0.4 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/vektah/gqlparser/v2 v2.5.1 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/zclconf/go-cty v1.10.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 // indirect diff --git a/go.sum b/go.sum index 09212d173..2617e60c8 100644 --- a/go.sum +++ b/go.sum @@ -909,6 +909,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a h1:Ey0XWvrg6u6hyIn1Kd/jCCmL+bMv9El81tvuGBbxZGg= +github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU= github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= @@ -1046,6 +1048,7 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= @@ -1069,6 +1072,8 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/pkg/bundle/installer.go b/pkg/bundle/installer.go index a5560f0d4..529bca586 100644 --- a/pkg/bundle/installer.go +++ b/pkg/bundle/installer.go @@ -73,13 +73,22 @@ func doInstall(client api.Client, recipe *api.Recipe, repo, name string, refresh return err } + posthogProperty := utils.PosthogProperties{ + ApplicationName: repo, + RecipeName: recipe.Name, + } if err := performTests(context, recipe); err != nil { + posthogProperty.Error = fmt.Errorf("failed to perform tests") + utils.PosthogCapture(utils.InstallPosthogEvent, posthogProperty) return err } if err := client.InstallRecipe(recipe.Id); err != nil { + posthogProperty.Error = fmt.Errorf("failed install recipe") + utils.PosthogCapture(utils.InstallPosthogEvent, posthogProperty) return fmt.Errorf("Install failed, does your plural user have install permissions? error: %w", api.GetErrorResponse(err, "InstallRecipe")) } + utils.PosthogCapture(utils.InstallPosthogEvent, posthogProperty) if recipe.OidcSettings == nil { return nil diff --git a/pkg/config/config.go b/pkg/config/config.go index 2e96cbeaf..404b9e4e1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -32,6 +32,7 @@ type Metadata struct { type Config struct { Email string `json:"email"` + ID string `yaml:"id" json:"id"` Token string `yaml:"token" json:"token"` NamespacePrefix string `yaml:"namespacePrefix"` Endpoint string `yaml:"endpoint"` diff --git a/pkg/manifest/types.go b/pkg/manifest/types.go index 0b5219504..303891db1 100644 --- a/pkg/manifest/types.go +++ b/pkg/manifest/types.go @@ -40,6 +40,7 @@ type Manifest struct { type Owner struct { Email string + ID string Endpoint string `yaml:"endpoint,omitempty"` } @@ -54,6 +55,7 @@ type ProjectManifest struct { Project string Provider string Region string + SendMetrics bool Owner *Owner Network *NetworkConfig BucketPrefix string `yaml:"bucketPrefix"` diff --git a/pkg/scaffold/scaffold.go b/pkg/scaffold/scaffold.go index dc4e81300..21d86715e 100644 --- a/pkg/scaffold/scaffold.go +++ b/pkg/scaffold/scaffold.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/pluralsh/plural/pkg/executor" + "github.com/pluralsh/plural/pkg/utils" "github.com/pluralsh/plural/pkg/utils/git" "github.com/pluralsh/plural/pkg/utils/pathing" "github.com/pluralsh/plural/pkg/wkspace" @@ -120,6 +121,17 @@ func (s *Scaffold) Execute(wk *wkspace.Workspace, force bool) error { preflight.Sha = "" } + for _, ch := range wk.Charts { + utils.PosthogCapture(utils.BuildPosthogEvent, utils.PosthogProperties{ + ApplicationName: wk.Installation.Repository.Name, + ApplicationID: wk.Installation.Id, + PackageType: s.Type, + PackageName: ch.Chart.Name, + PackageId: ch.Chart.Id, + PackageVersion: ch.Version.Version, + }) + } + sha, err := preflight.Execute(s.Root, ignore) if err != nil { return err diff --git a/pkg/utils/posthog.go b/pkg/utils/posthog.go new file mode 100644 index 000000000..5ef761e73 --- /dev/null +++ b/pkg/utils/posthog.go @@ -0,0 +1,122 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pluralsh/plural/pkg/utils/pathing" + "github.com/posthog/posthog-go" + "gopkg.in/yaml.v2" +) + +const ( + versionPlaceholder = "dev" +) + +var ( + version = versionPlaceholder +) + +type PosthogEvent string + +const ( + InstallPosthogEvent PosthogEvent = "cli_install" + BuildPosthogEvent PosthogEvent = "cli_build" + DeployPosthogEvent PosthogEvent = "cli_deploy" +) + +var posthogClient posthog.Client + +type ProjectInfo struct { + Spec *ProjectInfoSpec +} + +type ProjectInfoSpec struct { + SendMetrics bool + Cluster string + Provider string + Owner *ProjectInfoOwner +} +type ProjectInfoOwner struct { + ID string +} + +type PosthogProperties struct { + ApplicationName string `json:"applicationName,omitempty"` + ApplicationID string `json:"applicationID,omitempty"` + PackageType string `json:"packageType,omitempty"` + PackageName string `json:"packageName,omitempty"` + PackageId string `json:"packageID,omitempty"` + PackageVersion string `json:"packageVersion,omitempty"` + RecipeName string `json:"recipeName,omitempty"` + Error error `json:"error,omitempty"` +} + +func newPosthogClient() (posthog.Client, error) { + return posthog.NewWithConfig("phc_r0v4jbKz8Rr27mfqgO15AN5BMuuvnU8hCFedd6zpSDy", posthog.Config{ + Endpoint: "https://posthog.plural.sh", + }) +} + +func posthogCapture(posthogClient posthog.Client, event PosthogEvent, property PosthogProperties) error { + if project, err := getProjectInfo(); err == nil && project.Spec != nil && project.Spec.SendMetrics { + var properties map[string]interface{} + inrec, err := json.Marshal(property) + if err != nil { + return err + } + err = json.Unmarshal(inrec, &properties) + if err != nil { + return err + } + properties["clusterName"] = project.Spec.Cluster + properties["provider"] = project.Spec.Provider + properties["cliVersion"] = version + userID := "cli-user" + if project.Spec.Owner != nil { + userID = project.Spec.Owner.ID + } + LogInfo().Printf("send posthog event %v \n", properties) + return posthogClient.Enqueue(posthog.Capture{ + DistinctId: userID, + Event: string(event), + Properties: properties, + }) + } + LogInfo().Println("sending events disabled") + return nil +} + +func PosthogCapture(event PosthogEvent, property PosthogProperties) { + if posthogClient == nil { + var err error + posthogClient, err = newPosthogClient() + if err != nil { + LogError().Printf("Failed to create posthog client %v", err) + return + } + } + if err := posthogCapture(posthogClient, event, property); err != nil { + LogError().Printf("Failed to send posthog event %v", err) + } +} + +func getProjectInfo() (*ProjectInfo, error) { + root, found := ProjectRoot() + if found { + path := pathing.SanitizeFilepath(filepath.Join(root, "workspace.yaml")) + contents, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("could not find workspace.yaml file") + } + var projectInfo ProjectInfo + err = yaml.Unmarshal(contents, &projectInfo) + if err != nil { + return nil, err + } + return &projectInfo, nil + } + + return nil, fmt.Errorf("you are not in the project directory") +}