Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add telemetry to the CLI #958

Merged
merged 29 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1ecfa71
feat: Created telemetry client
JulesFaucherre Jun 27, 2023
cb04a0b
test: Written unit test for telemetry
JulesFaucherre Jun 27, 2023
a68d639
style: PR comments
JulesFaucherre Jul 5, 2023
91caef4
feat: Added `circleci telemetry enable` and `circleci telemetry disab…
JulesFaucherre Jul 5, 2023
71593f3
chore: Added telemetry events
JulesFaucherre Jul 6, 2023
9bacba5
chore: Added events for update, version and diagnostic
JulesFaucherre Jul 10, 2023
c0c284e
chore: Added telemetry events for completion, config, diagnostic, fol…
JulesFaucherre Jul 11, 2023
f0c6f29
chore: Moved create telemetry in its own module
JulesFaucherre Jul 11, 2023
2761dc6
chore: Added events for build, namespace and orb
JulesFaucherre Jul 11, 2023
051386b
chore: Added events for policy
JulesFaucherre Jul 11, 2023
4837c99
chore: Added events on runner
JulesFaucherre Jul 11, 2023
7f0184b
chore: Improve telemetry handling for config commands
JulesFaucherre Jul 11, 2023
7e7f1ee
fix: Version still run if you have no telemetry
JulesFaucherre Jul 11, 2023
dbd1239
inject Segment information at build time
JulesFaucherre Jul 12, 2023
7d36b51
test: Improve the way telemetry is tested
JulesFaucherre Jul 12, 2023
3261245
minor improvements
JulesFaucherre Jul 12, 2023
25c6676
fix lint
JulesFaucherre Jul 12, 2023
f79234b
fix: Added event for info command
JulesFaucherre Jul 13, 2023
c357de6
addressed minor pr comments
JulesFaucherre Jul 13, 2023
eaf3f6b
moved telemetry in command context
JulesFaucherre Jul 16, 2023
f6d26cc
last changes
JulesFaucherre Jul 18, 2023
c2680d8
minor changes on events and build
JulesFaucherre Jul 20, 2023
c6a3844
Put segment key in the code
JulesFaucherre Jul 20, 2023
299a285
fix tests
JulesFaucherre Jul 24, 2023
092470d
fix telemetry key
JulesFaucherre Jul 24, 2023
d53a66c
fix test for windows
JulesFaucherre Jul 24, 2023
a1491d1
Add telemetry information in docs
JulesFaucherre Jul 25, 2023
c71d485
Changed the value of the anonymous user id
JulesFaucherre Aug 1, 2023
1e32523
added event on command and fixed some tests
JulesFaucherre Aug 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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