Skip to content

Commit

Permalink
Merge pull request #958 from CircleCI-Public/task/DEVEX-898/cli-telem…
Browse files Browse the repository at this point in the history
…etry

Add telemetry to the CLI
  • Loading branch information
JulesFaucherre authored Aug 2, 2023
2 parents 16345fd + 1e32523 commit 1d65ffc
Show file tree
Hide file tree
Showing 49 changed files with 2,241 additions and 88 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ GOOS=$(shell go env GOOS)
GOARCH=$(shell go env GOARCH)

build: always
go build -o build/$(GOOS)/$(GOARCH)/circleci
go build -o build/$(GOOS)/$(GOARCH)/circleci -ldflags='-X github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=https://api.segment.io'

build-all: build/linux/amd64/circleci build/darwin/amd64/circleci

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,8 @@ Please see the [documentation](https://circleci-public.github.io/circleci-cli) o
| Functionality | Impacted commands | Change description | Compatibility with Server |
| --- | --- | --- | --- |
| Config compilation and validation | <ul><li>`circleci config validate`</li><li>`circleci config process`</li><li>`circleci local execute`</li> | The config validation has been moved from the GraphQL API to a specific API endpoint | <ul><li>**Server v4.0.5, v4.1.3, v4.2.0 and above**: Commands use the new specific endpoint</li><li>**Previous version**: Commands use the GraphQL API</li></ul> |
| Orb compilation and validation of orb using private orbs | <ul><li>`circleci orb process`</li><li>`circleci orb validate`</li></ul> | To support the validation of orbs requesting private orbs (see [issue](https://github.com/CircleCI-Public/circleci-cli/issues/751)). A field `ownerId` has been added to the GraphQL orb validation endpoint. Thus allowing the `Impacted commands` to use the `--org-id` parameter to enable the orb compilation / validation | <ul><li>**Server v4.2.0 and above**: The field is accessible so you can use the parameter</li><li>**Previous versions**: The field does not exist making the functionality unavailable</li></ul> |
| Orb compilation and validation of orb using private orbs | <ul><li>`circleci orb process`</li><li>`circleci orb validate`</li></ul> | To support the validation of orbs requesting private orbs (see [issue](https://github.com/CircleCI-Public/circleci-cli/issues/751)). A field `ownerId` has been added to the GraphQL orb validation endpoint. Thus allowing the `Impacted commands` to use the `--org-id` parameter to enable the orb compilation / validation | <ul><li>**Server v4.2.0 and above**: The field is accessible so you can use the parameter</li><li>**Previous versions**: The field does not exist making the functionality unavailable</li></ul> |

## Telemetry

There is some telemetry in the CLI. Be assured that we tried to keep it as little as possible. The first time you will run a command with the CLI you will be asked for your approval for the telemetry events. If your STDIN is not a TTY the telemetry will be automatically disabled, making it easier to use the CLI in scripts. Would you decide to change your mind about the telemetry, you can run the commands `circleci telemetry enable` and `circleci telemetry disable`. Setting the env variable `CIRCLECI_CLI_TELEMETRY_OPTOUT` will also disable the telemetry.
10 changes: 6 additions & 4 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,16 @@ tasks:
build:
desc: Build main
cmds:
- go build -v -o build/darwin/amd64/circleci .

# LDFlags sets the segment endpoint to an empty string thus letting the analytics library set the default endpoint on its own
# Not setting the `SegmentEndpoint` variable would let the value in the code ie "http://localhost"
- go build -v -o build/$(go env GOOS)/$(go env GOARCH)/circleci -ldflags='-X github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=https://api.segment.io' .

build-linux:
desc: Build main
cmds:
- go build -v -o build/linux/amd64/circleci .

cover:
desc: tests and generates a cover profile
cmds:
- TESTING=true go test -race -coverprofile=coverage.txt ./...
- TESTING=true go test -race -coverprofile=coverage.txt ./...
19 changes: 19 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"io"
"log"
"net/http"
"net/url"
"os"
"sort"
"strings"

"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/references"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/Masterminds/semver"
Expand Down Expand Up @@ -1945,3 +1947,20 @@ func FollowProject(config settings.Config, vcs string, owner string, projectName

return fr, nil
}

type Me struct {
ID string `json:"id"`
Login string `json:"login"`
Name string `json:"name"`
}

func GetMe(client *rest.Client) (Me, error) {
req, err := client.NewRequest("GET", &url.URL{Path: "me"}, nil)
if err != nil {
return Me{}, errors.Wrap(err, "Unable to get user info")
}

var me Me
_, err = client.DoRequest(req, &me)
return me, err
}
22 changes: 18 additions & 4 deletions clitest/clitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (
"runtime"

"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/onsi/gomega/gexec"
"github.com/onsi/gomega/ghttp"
"github.com/onsi/gomega/types"
"gopkg.in/yaml.v3"

"github.com/onsi/gomega"
)
Expand All @@ -30,10 +32,12 @@ func ShouldFail() types.GomegaMatcher {

// TempSettings contains useful settings for testing the CLI
type TempSettings struct {
Home string
TestServer *ghttp.Server
Config *TmpFile
Update *TmpFile
Home string
TestServer *ghttp.Server
Config *TmpFile
Update *TmpFile
Telemetry *TmpFile
TelemetryDestPath string
}

// Close should be called in an AfterEach and cleans up the temp directory and server process
Expand Down Expand Up @@ -68,6 +72,16 @@ func WithTempSettings() *TempSettings {
gomega.Expect(os.Mkdir(settingsPath, 0700)).To(gomega.Succeed())

tempSettings.Config = OpenTmpFile(settingsPath, "cli.yml")
tempSettings.Telemetry = OpenTmpFile(settingsPath, "telemetry.yml")
content, err := yaml.Marshal(settings.TelemetrySettings{
IsEnabled: false,
HasAnsweredPrompt: true,
})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
_, err = tempSettings.Telemetry.File.Write(content)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
tempSettings.TelemetryDestPath = filepath.Join(tempSettings.Home, "telemetry-content")

tempSettings.Update = OpenTmpFile(settingsPath, "update_check.yml")

tempSettings.TestServer = ghttp.NewServer()
Expand Down
19 changes: 19 additions & 0 deletions clitest/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package clitest

import (
"encoding/json"
"os"

"github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/onsi/gomega"
)

func CompareTelemetryEvent(settings *TempSettings, expected []telemetry.Event) {
content, err := os.ReadFile(settings.TelemetryDestPath)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

result := []telemetry.Event{}
err = json.Unmarshal(content, &result)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(result).To(gomega.Equal(expected))
}
10 changes: 9 additions & 1 deletion cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"github.com/CircleCI-Public/circleci-cli/local"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/spf13/cobra"
)

Expand All @@ -16,7 +17,14 @@ func newLocalExecuteCommand(config *settings.Config) *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, _ []string) error {
return local.Execute(cmd.Flags(), config, args)
err := local.Execute(cmd.Flags(), config, args)

telemetryClient, ok := telemetry.FromContext(cmd.Context())
if ok {
_ = telemetryClient.Track(telemetry.CreateLocalExecuteEvent(telemetry.GetCommandInformation(cmd, true)))
}

return err
},
Args: cobra.MinimumNArgs(1),
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/cmd_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"testing"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
Expand All @@ -14,6 +15,8 @@ import (
var pathCLI string

var _ = BeforeSuite(func() {
SetDefaultEventuallyTimeout(time.Second * 30)

var err error
pathCLI, err = gexec.Build("github.com/CircleCI-Public/circleci-cli")
Ω(err).ShouldNot(HaveOccurred())
Expand Down
10 changes: 9 additions & 1 deletion cmd/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ 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, ok := telemetry.FromContext(cmd.Context())
if ok {
_ = telemetryClient.Track(telemetry.CreateCompletionCommand(telemetry.GetCommandInformation(cmd, true)))
}
},
Run: func(cmd *cobra.Command, _ []string) {
err := cmd.Help()
if err != nil {
Expand Down
31 changes: 24 additions & 7 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import (
"github.com/CircleCI-Public/circleci-config/labeling"
"github.com/CircleCI-Public/circleci-config/labeling/codebase"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/CircleCI-Public/circleci-cli/config"
"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.
Expand All @@ -37,8 +37,14 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
packCommand := &cobra.Command{
Use: "pack <path>",
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 {
err := packConfig(args)

telemetryClient, ok := telemetry.FromContext(cmd.Context())
if ok {
_ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err))
}
return err
},
Args: cobra.ExactArgs(1),
Annotations: make(map[string]string),
Expand All @@ -60,13 +66,20 @@ 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, ok := telemetry.FromContext(cmd.Context())
if ok {
_ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err))
}

return err
},
Args: cobra.MaximumNArgs(1),
Annotations: make(map[string]string),
Expand Down Expand Up @@ -104,6 +117,10 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
PipelineParamsFilePath: pipelineParamsFilePath,
VerboseOutput: verboseOutput,
})
telemetryClient, ok := telemetry.FromContext(cmd.Context())
if ok {
_ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err))
}
if err != nil {
return err
}
Expand Down
32 changes: 31 additions & 1 deletion cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,12 +30,41 @@ var _ = Describe("Config", func() {
tempSettings.Close()
})

Describe("telemetry", func() {
BeforeEach(func() {
tempSettings = clitest.WithTempSettings()
command = commandWithHome(pathCLI, tempSettings.Home,
"config", "pack",
"--skip-update-check",
filepath.Join("testdata", "hugo-pack", ".circleci"),
)
command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
})

AfterEach(func() {
tempSettings.Close()
})

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(tempSettings, []telemetry.Event{
telemetry.CreateConfigEvent(telemetry.CommandInfo{
Name: "pack",
LocalArgs: map[string]string{"help": "false"},
}, nil),
})
})
})

Describe("a .circleci folder with config.yml and local orbs folder containing the hugo orb", func() {
BeforeEach(func() {
command = exec.Command(pathCLI,
"config", "pack",
"--skip-update-check",
"testdata/hugo-pack/.circleci")
filepath.Join("testdata", "hugo-pack", ".circleci"))
results = golden.Get(GinkgoT(), filepath.FromSlash("hugo-pack/result.yml"))
})

Expand Down
Loading

0 comments on commit 1d65ffc

Please sign in to comment.