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

Reintroduce telemetry #1004

Merged
merged 3 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ jobs:
working_directory: integration_tests
environment:
TESTING: "true"
- run:
name: "Make sure simple command do not cause any timeout"
command: circleci version

test:
executor: go
steps:
Expand Down Expand Up @@ -378,12 +382,14 @@ workflows:
- devex-release
- deploy:
requires:
- cucumber
- test
- test_mac
- coverage
- lint
- deploy-test
- shellcheck/check
- vulnerability-scan
filters:
branches:
only: main
Expand Down
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
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ Please see the [documentation](https://circleci-public.github.io/circleci-cli) o

## Server compatibility

There are some difference of behavior depending on the version you use:
- config validation will use the GraphQL API until **Server v4.0.5, v4.1.3, v4.2.0**. The above versions will use the new route `compile-config-with-defaults`
- `circleci orb validate` will only allow you to validate orbs using other private orbs with the option `--org-slug` from version **Server v4.2.0**
| 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> |

## Telemetry

The CircleCI CLI includes a telemetry feature that collects basic errors and feature usage data in order to help us improve the experience for everyone.

Telemetry works on an opt-in basis: when running a command for the first time, you will be asked for consent to enable telemetry. For non-TTY STDIN, telemetry is disabled by default, ensuring that scripts that use the CLI run smoothly.

You can disable or enable telemetry anytime in one of the following ways:

* Run the commands `circleci telemetry enable` or `circleci telemetry disable`

* Set the `CIRCLECI_CLI_TELEMETRY_OPTOUT` environment variable to `1` or `true` to disable it

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 @@ -1818,3 +1820,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 @@ -64,13 +70,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 @@ -112,6 +125,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