Skip to content

Commit

Permalink
chore: Added telemetry events
Browse files Browse the repository at this point in the history
  • Loading branch information
JulesFaucherre committed Aug 1, 2023
1 parent 91caef4 commit 71593f3
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 24 deletions.
10 changes: 10 additions & 0 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 Down Expand Up @@ -70,6 +72,14 @@ func WithTempSettings() *TempSettings {

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

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

tempSettings.TestServer = ghttp.NewServer()
Expand Down
19 changes: 15 additions & 4 deletions cmd/create_telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,27 @@ func (client telemetryCircleCIAPI) getMyUserId() (string, error) {
return me.ID, nil
}

type nullTelemetryAPIClient struct{}

func (client nullTelemetryAPIClient) getMyUserId() (string, error) {
panic("Should not be called")
}

// Make sure the user gave their approval for the telemetry and
func createTelemetry(config *settings.Config) telemetry.Client {
if config.MockTelemetry != "" {
return telemetry.CreateFileTelemetry(config.MockTelemetry)
}

if config.IsTelemetryDisabled {
return telemetry.CreateClient(telemetry.User{}, false)
}

apiClient := telemetryCircleCIAPI{
cli: rest.NewFromConfig(config.Host, config),
var apiClient telemetryAPIClient = nullTelemetryAPIClient{}
if config.HTTPClient != nil {
apiClient = telemetryCircleCIAPI{
cli: rest.NewFromConfig(config.Host, config),
}
}
ui := telemetryInteractiveUI{}

Expand Down Expand Up @@ -150,6 +163,4 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry
if err := settings.Write(); err != nil {
fmt.Printf("Error writing telemetry settings to disk: %s\n", err)
}

return
}
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,9 @@ func MakeCommands() *cobra.Command {
flags.StringVar(&rootOptions.GitHubAPI, "github-api", "https://api.github.com/", "Change the default endpoint to GitHub API for retrieving updates")
flags.BoolVar(&rootOptions.SkipUpdateCheck, "skip-update-check", skipUpdateByDefault(), "Skip the check for updates check run before every command.")
flags.BoolVar(&rootOptions.IsTelemetryDisabled, "disable-telemetry", false, "Do not show telemetry for the actual command")
flags.StringVar(&rootOptions.MockTelemetry, "mock-telemetry", "", "The path where telemetry must be written")

hidden := []string{"github-api", "debug", "endpoint"}
hidden := []string{"github-api", "debug", "endpoint", "mock-telemetry"}

for _, f := range hidden {
if err := flags.MarkHidden(f); err != nil {
Expand Down
7 changes: 7 additions & 0 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/prompt"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -189,6 +190,12 @@ func setup(opts setupOptions) error {
setupDiagnosticCheck(opts)
}

telemetryClient := createTelemetry(opts.cfg)
defer telemetryClient.Close()
if err := telemetryClient.Track(telemetry.CreateSetupEvent(opts.cfg.Host == defaultHost)); err != nil {
fmt.Printf("Unable to send telemetry event: %s\n", err)
}

return nil
}

Expand Down
36 changes: 36 additions & 0 deletions cmd/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"

Expand Down Expand Up @@ -35,6 +36,41 @@ var _ = Describe("Setup with prompts", func() {
tempSettings.Close()
})

Describe("telemetry", func() {
var (
telemetryDestFilePath string
)

BeforeEach(func() {
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)
}
})

It("should send telemetry event when", func() {
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())

Eventually(session).Should(gexec.Exit(0))
content, err := os.ReadFile(telemetryDestFilePath)
Expect(err).ShouldNot(HaveOccurred())
Expect(string(content)).To(Equal(`{"object":"cli-setup","action":"called","properties":{"is_server_customer":false}}
`))
})
})

Describe("new config file", func() {
It("should set file permissions to 0600", func() {
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expand Down
9 changes: 9 additions & 0 deletions cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/CircleCI-Public/circleci-cli/version"
"github.com/spf13/cobra"
)
Expand All @@ -17,11 +18,13 @@ func newVersionCommand(config *settings.Config) *cobra.Command {
opts := versionOptions{
cfg: config,
}
var telemetryClient telemetry.Client

return &cobra.Command{
Use: "version",
Short: "Display version information",
PersistentPreRun: func(_ *cobra.Command, _ []string) {
telemetryClient = createTelemetry(config)
opts.cfg.SkipUpdateCheck = true
},
PreRun: func(cmd *cobra.Command, args []string) {
Expand All @@ -30,5 +33,11 @@ func newVersionCommand(config *settings.Config) *cobra.Command {
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("%s+%s (%s)\n", version.Version, version.Commit, version.PackageManager())
},
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := telemetryClient.Track(telemetry.CreateVersionEvent()); err != nil {
return err
}
return telemetryClient.Close()
},
}
}
35 changes: 19 additions & 16 deletions settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,25 @@ var (

// Config is used to represent the current state of a CLI instance.
type Config struct {
Host string `yaml:"host"`
DlHost string `yaml:"-"`
Endpoint string `yaml:"endpoint"`
Token string `yaml:"token"`
RestEndpoint string `yaml:"rest_endpoint"`
TLSCert string `yaml:"tls_cert"`
TLSInsecure bool `yaml:"tls_insecure"`
HTTPClient *http.Client `yaml:"-"`
Data *data.DataBag `yaml:"-"`
Debug bool `yaml:"-"`
Address string `yaml:"-"`
FileUsed string `yaml:"-"`
GitHubAPI string `yaml:"-"`
SkipUpdateCheck bool `yaml:"-"`
IsTelemetryDisabled bool `yaml:"-"`
OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"`
Host string `yaml:"host"`
DlHost string `yaml:"-"`
Endpoint string `yaml:"endpoint"`
Token string `yaml:"token"`
RestEndpoint string `yaml:"rest_endpoint"`
TLSCert string `yaml:"tls_cert"`
TLSInsecure bool `yaml:"tls_insecure"`
HTTPClient *http.Client `yaml:"-"`
Data *data.DataBag `yaml:"-"`
Debug bool `yaml:"-"`
Address string `yaml:"-"`
FileUsed string `yaml:"-"`
GitHubAPI string `yaml:"-"`
SkipUpdateCheck bool `yaml:"-"`
IsTelemetryDisabled bool `yaml:"-"`
// If this value is defined, the telemetry will write all its events a file
// The value of this field is the path where the telemetry will be written
MockTelemetry string `yaml:"-"`
OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"`
}

type OrbPublishingInfo struct {
Expand Down
24 changes: 24 additions & 0 deletions telemetry/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package telemetry

// This file contains all the telemetry event constructors
// All the events are referenced in the following file:
// https://circleci.atlassian.net/wiki/spaces/DE/pages/6760694125/CLI+segment+event+tracking
// If you want to add an event, first make sure it appears in this file

func CreateSetupEvent(isServerCustomer bool) Event {
return Event{
Object: "cli-setup",
Action: "called",
Properties: map[string]interface{}{
"is_server_customer": isServerCustomer,
},
}
}

func CreateVersionEvent() Event {
return Event{
Object: "cli-version",
Action: "called",
Properties: map[string]interface{}{},
}
}
45 changes: 42 additions & 3 deletions telemetry/telemetry.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package telemetry

import (
"encoding/json"
"fmt"
"io"
"os"

"github.com/segmentio/analytics-go"
)
Expand All @@ -26,10 +28,13 @@ type Client interface {
Track(event Event) error
}

// A segment event to be sent to the telemetry
// Important: this is not meant to be constructed directly apart in tests
// If you want to create a new event, add its constructor in ./events.go
type Event struct {
Object string
Action string
Properties map[string]interface{}
Object string `json:"object"`
Action string `json:"action"`
Properties map[string]interface{} `json:"properties"`
}

type User struct {
Expand All @@ -43,6 +48,7 @@ type User struct {

// Create a telemetry client to be used to send telemetry events
func CreateClient(user User, enabled bool) Client {
fmt.Printf("telemetry enabled = %+v\n", enabled)
if !enabled {
return nullClient{}
}
Expand Down Expand Up @@ -138,3 +144,36 @@ func (segment *segmentClient) Track(event Event) error {
func (segment *segmentClient) Close() error {
return segment.cli.Close()
}

// File telemetry
// Used for E2E tests

type fileTelemetry struct {
file *os.File
}

func CreateFileTelemetry(filePath string) Client {
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
panic(err)
}
return &fileTelemetry{file}
}

func (cli *fileTelemetry) Track(event Event) error {
content, err := json.Marshal(&event)
if err != nil {
return err
}

content = append(content, '\n')
_, err = cli.file.Write(content)

return err
}

func (cli *fileTelemetry) Close() error {
file := cli.file
cli.file = nil
return file.Close()
}

0 comments on commit 71593f3

Please sign in to comment.