From c0c284e1b33e9ab7ebcf8f1b8f55d206e257fce8 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 11 Jul 2023 10:28:59 +0200 Subject: [PATCH] chore: Added telemetry events for completion, config, diagnostic, follow and open --- cmd/completion.go | 9 +++- cmd/config.go | 37 ++++++++++++-- cmd/config_test.go | 37 ++++++++++++++ cmd/diagnostic.go | 12 +++-- cmd/diagnostic_test.go | 29 +++++++++-- cmd/follow.go | 9 +++- cmd/open.go | 14 ++++-- cmd/root.go | 4 +- cmd/setup_test.go | 64 +++++++++++++------------ cmd/testdata/config_validate/config.yml | 16 +++++++ telemetry/events.go | 39 ++++++++++++++- 11 files changed, 218 insertions(+), 52 deletions(-) create mode 100644 cmd/testdata/config_validate/config.yml diff --git a/cmd/completion.go b/cmd/completion.go index bc0ff6d53..8346852e0 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -3,13 +3,20 @@ package cmd import ( "os" + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" ) -func newCompletionCommand() *cobra.Command { +func newCompletionCommand(config *settings.Config) *cobra.Command { completionCmd := &cobra.Command{ Use: "completion", Short: "Generate shell completion scripts", + PersistentPreRun: func(cmd *cobra.Command, _ []string) { + telemetryClient := createTelemetry(config) + defer telemetryClient.Close() + telemetryClient.Track(telemetry.CreateCompletionCommand(getCommandInformation(cmd, false))) + }, Run: func(cmd *cobra.Command, _ []string) { err := cmd.Help() if err != nil { diff --git a/cmd/config.go b/cmd/config.go index 6c85de763..da669e54b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -16,6 +16,10 @@ import ( "github.com/CircleCI-Public/circleci-cli/filetree" "github.com/CircleCI-Public/circleci-cli/proxy" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" ) // Path to the config.yml file to operate on. @@ -29,16 +33,34 @@ var configAnnotations = map[string]string{ } func newConfigCommand(globalConfig *settings.Config) *cobra.Command { + var telemetryClient telemetry.Client + + closeTelemetryClient := func() { + if telemetryClient != nil { + telemetryClient.Close() + telemetryClient = nil + } + } + configCmd := &cobra.Command{ Use: "config", Short: "Operate on build config files", + PersistentPreRun: func(_ *cobra.Command, _ []string) { + telemetryClient = createTelemetry(globalConfig) + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + closeTelemetryClient() + }, } packCommand := &cobra.Command{ Use: "pack ", Short: "Pack up your CircleCI configuration into a single file.", - RunE: func(_ *cobra.Command, args []string) error { - return packConfig(args) + RunE: func(cmd *cobra.Command, args []string) error { + defer closeTelemetryClient() + err := packConfig(args) + telemetryClient.Track(telemetry.CreateConfigEvent(getCommandInformation(cmd, true))) + return err }, Args: cobra.ExactArgs(1), Annotations: make(map[string]string), @@ -50,6 +72,8 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { Aliases: []string{"check"}, Short: "Check that the config file is well formed.", RunE: func(cmd *cobra.Command, args []string) error { + defer closeTelemetryClient() + compiler := config.New(globalConfig) orgID, _ := cmd.Flags().GetString("org-id") orgSlug, _ := cmd.Flags().GetString("org-slug") @@ -60,13 +84,17 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { if len(args) == 1 { path = args[0] } - return compiler.ValidateConfig(config.ValidateConfigOpts{ + + err := compiler.ValidateConfig(config.ValidateConfigOpts{ ConfigPath: path, OrgID: orgID, OrgSlug: orgSlug, IgnoreDeprecatedImages: ignoreDeprecatedImages, VerboseOutput: verboseOutput, }) + telemetryClient.Track(telemetry.CreateConfigEvent(getCommandInformation(cmd, true))) + + return err }, Args: cobra.MaximumNArgs(1), Annotations: make(map[string]string), @@ -86,6 +114,8 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { Use: "process ", Short: "Validate config and display expanded configuration.", RunE: func(cmd *cobra.Command, args []string) error { + defer closeTelemetryClient() + compiler := config.New(globalConfig) pipelineParamsFilePath, _ := cmd.Flags().GetString("pipeline-parameters") orgID, _ := cmd.Flags().GetString("org-id") @@ -97,6 +127,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { if len(args) == 1 { path = args[0] } + telemetryClient.Track(telemetry.CreateConfigEvent(getCommandInformation(cmd, true))) response, err := compiler.ProcessConfig(config.ProcessConfigOpts{ ConfigPath: path, OrgID: orgID, diff --git a/cmd/config_test.go b/cmd/config_test.go index 9168b4146..b48770cdf 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" @@ -29,6 +30,42 @@ var _ = Describe("Config", func() { tempSettings.Close() }) + Describe("telemetry", func() { + var telemetryDestFilePath string + + BeforeEach(func() { + telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") + + tempSettings = clitest.WithTempSettings() + command = commandWithHome(pathCLI, tempSettings.Home, + "config", "pack", + "--skip-update-check", + "testdata/hugo-pack/.circleci", + "--mock-telemetry", telemetryDestFilePath, + ) + }) + + AfterEach(func() { + tempSettings.Close() + if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { + os.Remove(telemetryDestFilePath) + } + }) + + It("should send telemetry event", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + telemetry.CreateConfigEvent(telemetry.CommandInfo{ + Name: "pack", + LocalArgs: map[string]string{"help": "false"}, + }), + }) + }) + }) + Describe("a .circleci folder with config.yml and local orbs folder containing the hugo orb", func() { BeforeEach(func() { command = exec.Command(pathCLI, diff --git a/cmd/diagnostic.go b/cmd/diagnostic.go index 9624c35c9..24dce3251 100644 --- a/cmd/diagnostic.go +++ b/cmd/diagnostic.go @@ -26,15 +26,17 @@ func newDiagnosticCommand(config *settings.Config) *cobra.Command { Use: "diagnostic", Short: "Check the status of your CircleCI CLI.", PreRun: func(cmd *cobra.Command, args []string) { - telemetryClient := createTelemetry(config) - defer telemetryClient.Close() - telemetryClient.Track(telemetry.CreateDiagnosticEvent()) - opts.args = args opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) }, RunE: func(_ *cobra.Command, _ []string) error { - return diagnostic(opts) + telemetryClient := createTelemetry(config) + defer telemetryClient.Close() + + err := diagnostic(opts) + telemetryClient.Track(telemetry.CreateDiagnosticEvent(err)) + + return err }, } diff --git a/cmd/diagnostic_test.go b/cmd/diagnostic_test.go index f3d48e5d5..d3ec07f94 100644 --- a/cmd/diagnostic_test.go +++ b/cmd/diagnostic_test.go @@ -3,11 +3,13 @@ package cmd_test import ( "fmt" "net/http" + "os" "os/exec" "path/filepath" "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" @@ -17,18 +19,21 @@ import ( var _ = Describe("Diagnostic", func() { var ( - tempSettings *clitest.TempSettings - command *exec.Cmd - defaultEndpoint = "graphql-unstable" + telemetryDestFilePath string + tempSettings *clitest.TempSettings + command *exec.Cmd + defaultEndpoint = "graphql-unstable" ) BeforeEach(func() { tempSettings = clitest.WithTempSettings() + telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") command = commandWithHome(pathCLI, tempSettings.Home, "diagnostic", "--skip-update-check", - "--host", tempSettings.TestServer.URL()) + "--host", tempSettings.TestServer.URL(), + "--mock-telemetry", telemetryDestFilePath) query := `query IntrospectionQuery { __schema { @@ -77,6 +82,22 @@ var _ = Describe("Diagnostic", func() { AfterEach(func() { tempSettings.Close() + if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { + os.Remove(telemetryDestFilePath) + } + }) + + Describe("telemetry", func() { + It("should send telemetry event", func() { + tempSettings.Config.Write([]byte(`token: mytoken`)) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + telemetry.CreateDiagnosticEvent(nil), + }) + }) }) Describe("existing config file", func() { diff --git a/cmd/follow.go b/cmd/follow.go index 31eb3bbae..e3afa40a5 100644 --- a/cmd/follow.go +++ b/cmd/follow.go @@ -6,6 +6,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/git" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -54,7 +55,13 @@ func followProjectCommand(config *settings.Config) *cobra.Command { Use: "follow", Short: "Attempt to follow the project for the current git repository.", RunE: func(_ *cobra.Command, _ []string) error { - return followProject(opts) + telemetryClient := createTelemetry(config) + defer telemetryClient.Close() + + err := followProject(opts) + telemetryClient.Track(telemetry.CreateFollowEvent(err)) + + return err }, } return followCommand diff --git a/cmd/open.go b/cmd/open.go index 6f1772b1d..d53c08440 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/CircleCI-Public/circleci-cli/git" + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/pkg/browser" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -13,7 +15,7 @@ import ( // errorMessage string containing the error message displayed in both the open command and the follow command var errorMessage = ` -This command is intended to be run from a git repository with a remote named 'origin' that is hosted on Github or Bitbucket only. +This command is intended to be run from a git repository with a remote named 'origin' that is hosted on Github or Bitbucket only. We are not currently supporting any other hosts.` // projectUrl uses the provided values to create the url to open @@ -39,12 +41,18 @@ func openProjectInBrowser() error { } // newOpenCommand creates the cli command open -func newOpenCommand() *cobra.Command { +func newOpenCommand(config *settings.Config) *cobra.Command { openCommand := &cobra.Command{ Use: "open", Short: "Open the current project in the browser.", RunE: func(_ *cobra.Command, _ []string) error { - return openProjectInBrowser() + telemetryClient := createTelemetry(config) + defer telemetryClient.Close() + + err := openProjectInBrowser() + _ = telemetryClient.Track(telemetry.CreateOpenEvent(err)) + + return err }, } return openCommand diff --git a/cmd/root.go b/cmd/root.go index c85759851..d3cfd3d5d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -144,7 +144,7 @@ func MakeCommands() *cobra.Command { return validateToken(rootOptions) } - rootCmd.AddCommand(newOpenCommand()) + rootCmd.AddCommand(newOpenCommand(rootOptions)) rootCmd.AddCommand(newTestsCommand()) rootCmd.AddCommand(newContextCommand(rootOptions)) rootCmd.AddCommand(project.NewProjectCommand(rootOptions, validator)) @@ -173,7 +173,7 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newStepCommand(rootOptions)) rootCmd.AddCommand(newSwitchCommand(rootOptions)) rootCmd.AddCommand(newAdminCommand(rootOptions)) - rootCmd.AddCommand(newCompletionCommand()) + rootCmd.AddCommand(newCompletionCommand(rootOptions)) rootCmd.AddCommand(newEnvCmd()) rootCmd.AddCommand(newTelemetryCommand(rootOptions)) diff --git a/cmd/setup_test.go b/cmd/setup_test.go index 939b7b31a..329077096 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -17,58 +17,60 @@ import ( "github.com/onsi/gomega/gexec" ) -var _ = Describe("Setup with prompts", func() { +var _ = Describe("Setup telemetry", func() { var ( - command *exec.Cmd - tempSettings *clitest.TempSettings + command *exec.Cmd + tempSettings *clitest.TempSettings + telemetryDestFilePath string ) BeforeEach(func() { tempSettings = clitest.WithTempSettings() - + telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") command = commandWithHome(pathCLI, tempSettings.Home, "setup", "--integration-testing", "--skip-update-check", + "--mock-telemetry", telemetryDestFilePath, ) }) AfterEach(func() { tempSettings.Close() + if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { + os.Remove(telemetryDestFilePath) + } }) - Describe("telemetry", func() { - var ( - telemetryDestFilePath string - ) - - BeforeEach(func() { - telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") + It("should send telemetry event", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) - command = commandWithHome(pathCLI, tempSettings.Home, - "setup", - "--integration-testing", - "--skip-update-check", - "--mock-telemetry", telemetryDestFilePath, - ) + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + telemetry.CreateSetupEvent(false), }) + }) +}) - AfterEach(func() { - tempSettings.Close() - if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { - os.Remove(telemetryDestFilePath) - } - }) +var _ = Describe("Setup with prompts", func() { + var ( + command *exec.Cmd + tempSettings *clitest.TempSettings + ) - It("should send telemetry event", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) + BeforeEach(func() { + tempSettings = clitest.WithTempSettings() - Eventually(session).Should(gexec.Exit(0)) - clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ - telemetry.CreateSetupEvent(false), - }) - }) + command = commandWithHome(pathCLI, tempSettings.Home, + "setup", + "--integration-testing", + "--skip-update-check", + ) + }) + + AfterEach(func() { + tempSettings.Close() }) Describe("new config file", func() { diff --git a/cmd/testdata/config_validate/config.yml b/cmd/testdata/config_validate/config.yml new file mode 100644 index 000000000..e6e7a4435 --- /dev/null +++ b/cmd/testdata/config_validate/config.yml @@ -0,0 +1,16 @@ +version: 2.1 + +orbs: + node: circleci/node@5.0.3 + +jobs: + hello-world: + docker: + - image: cimg/base:stable + steps: + - run: | + echo "Hello world!" +workflows: + datadog-hello-world: + jobs: + - hello-world diff --git a/telemetry/events.go b/telemetry/events.go index b01635fa3..c6a20dbff 100644 --- a/telemetry/events.go +++ b/telemetry/events.go @@ -20,6 +20,15 @@ func localArgsToProperties(cmdInfo CommandInfo) map[string]interface{} { return properties } +func errorToProperties(err error) map[string]interface{} { + if err == nil { + return nil + } + return map[string]interface{}{ + "error": err.Error(), + } +} + func CreateSetupEvent(isServerCustomer bool) Event { return Event{ Object: "cli-setup", @@ -46,8 +55,34 @@ func CreateUpdateEvent(cmdInfo CommandInfo) Event { } } -func CreateDiagnosticEvent() Event { +func CreateDiagnosticEvent(err error) Event { + return Event{ + Object: "cli-diagnostic", Properties: errorToProperties(err), + } +} + +func CreateFollowEvent(err error) Event { + return Event{ + Object: "cli-follow", Properties: errorToProperties(err), + } +} + +func CreateOpenEvent(err error) Event { + return Event{Object: "cli-open", Properties: errorToProperties(err)} +} + +func CreateCompletionCommand(cmdInfo CommandInfo) Event { + return Event{ + Object: "cli-completion", + Action: cmdInfo.Name, + Properties: localArgsToProperties(cmdInfo), + } +} + +func CreateConfigEvent(cmdInfo CommandInfo) Event { return Event{ - Object: "cli-diagnostic", + Object: "cli-config", + Action: cmdInfo.Name, + Properties: localArgsToProperties(cmdInfo), } }