From 7d9ca07600d4ec54dc484a3b3a6169e907d649b4 Mon Sep 17 00:00:00 2001 From: Stuart Reid Date: Mon, 26 Jun 2023 14:58:24 +0100 Subject: [PATCH 01/36] update runner component CODEOWNDER --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ec9fce118..e8f4d8c1b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ * @CircleCI-Public/developer-experience *orb*.go @CircleCI-Public/orb-publishers @CircleCI-Public/developer-experience -/api/runner @CircleCI-Public/runner -/cmd/runner @CircleCI-Public/runner +/api/runner @CircleCI-Public/on-prem +/cmd/runner @CircleCI-Public/on-prem \ No newline at end of file From fba2637e039abf3be9bb82f362771238f90a27b3 Mon Sep 17 00:00:00 2001 From: Camilo Polymeris Date: Fri, 30 Jun 2023 08:59:03 +0200 Subject: [PATCH 02/36] feat: Bump the circleci-config version Diff: https://github.com/CircleCI-Public/circleci-config/compare/182164ce950a...c469d9e9936b Includes: - Improvements to Go, Python, Node & Ruby - Added support for Rust & PHP --- cmd/config_test.go | 4 ++-- go.mod | 2 +- go.sum | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/config_test.go b/cmd/config_test.go index 3b1fb3089..9168b4146 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -257,7 +257,7 @@ var _ = Describe("Config", func() { session.Wait() Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchRegexp("npm run test")) + Eventually(session.Out.Contents()).Should(MatchRegexp("npm test")) Eventually(session).Should(gexec.Exit(0)) }) @@ -273,7 +273,7 @@ var _ = Describe("Config", func() { session.Wait() Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchRegexp("npm run test")) + Eventually(session.Out.Contents()).Should(MatchRegexp("npm test")) Eventually(session).Should(gexec.Exit(0)) }) }) diff --git a/go.mod b/go.mod index c47868be5..036ba1dd6 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( ) require ( - github.com/CircleCI-Public/circleci-config v0.0.0-20230609135034-182164ce950a + github.com/CircleCI-Public/circleci-config v0.0.0-20230629192034-c469d9e9936b github.com/a8m/envsubst v1.4.2 github.com/charmbracelet/lipgloss v0.5.0 github.com/erikgeiser/promptkit v0.7.0 diff --git a/go.sum b/go.sum index 43b9c4e41..c06fe807b 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ github.com/CircleCI-Public/circleci-config v0.0.0-20230609041455-d155ae7f0410 h1 github.com/CircleCI-Public/circleci-config v0.0.0-20230609041455-d155ae7f0410/go.mod h1:XZaQPj2ylXZaz5vW31dRdkUY/Ey8MdpbgrUHbHyzICY= github.com/CircleCI-Public/circleci-config v0.0.0-20230609135034-182164ce950a h1:RqA4H9p77FsqV++HNNDBq8dJftYuJ+r+KdD9HAX28t4= github.com/CircleCI-Public/circleci-config v0.0.0-20230609135034-182164ce950a/go.mod h1:XZaQPj2ylXZaz5vW31dRdkUY/Ey8MdpbgrUHbHyzICY= +github.com/CircleCI-Public/circleci-config v0.0.0-20230629192034-c469d9e9936b h1:emg7uU3bRjVMlwSpOATBiybaBPXNWUIiFE/qbQQXZtE= +github.com/CircleCI-Public/circleci-config v0.0.0-20230629192034-c469d9e9936b/go.mod h1:0iW5+XMF4XtikTlfCElaBQjT/OTMjQRHM1DgSWxcWuE= github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= From 62f7bd384b6e2ce579ef519ddf5308c9ea08603f Mon Sep 17 00:00:00 2001 From: Charles Francoise Date: Mon, 10 Jul 2023 14:29:13 +0200 Subject: [PATCH 03/36] fix: don't create license file when initializing private orb (#962) --- cmd/orb.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cmd/orb.go b/cmd/orb.go index 622387126..a7037c7d8 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -1218,7 +1218,8 @@ func initOrb(opts orbOptions) error { defer resp.Body.Close() // Create the file - out, err := os.Create(filepath.Join(os.TempDir(), "orb-template.zip")) + zipPath := filepath.Join(os.TempDir(), "orb-template.zip") + out, err := os.Create(zipPath) if err != nil { return err } @@ -1230,11 +1231,19 @@ func initOrb(opts orbOptions) error { return err } - err = unzipToOrbPath(filepath.Join(os.TempDir(), "orb-template.zip"), orbPath) + err = unzipToOrbPath(zipPath, orbPath) if err != nil { return err } + // Remove MIT License file if orb is private + if opts.private { + err = os.Remove(filepath.Join(orbPath, "LICENSE")) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + } + if fullyAutomated == 1 { fmt.Println("Opted for manual setup, exiting") fmt.Printf("The Orb Project Template has been extracted to %s\n", orbPath) From f0ee66b12eb6ff733cf04e082fbbbfc89b645c37 Mon Sep 17 00:00:00 2001 From: Charles Francoise Date: Mon, 10 Jul 2023 15:17:17 +0200 Subject: [PATCH 04/36] feat: build & release darwin/arm64 binary (#961) * update goreleaser version * install goreleaaser with apt * fix version * make apt quieter * fix dockerfiles * increase build executor resource class * remove cache --- .circleci/config.yml | 32 ++++++++++++++------------------ Dockerfile | 2 +- Dockerfile.alpine | 2 +- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7485d0566..e2fa808e6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,6 +8,7 @@ executors: go: docker: - image: cimg/go:1.20 + resource_class: large environment: CGO_ENABLED: 0 mac: @@ -25,7 +26,7 @@ commands: # https://app.circleci.com/jobs/github/CircleCI-Public/circleci-cli/6480 # curl: (92) HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1) # The issue seems to be on the server-side, so force HTTP 1.1 - name: 'cURL: Force HTTP 1.1' + name: "cURL: Force HTTP 1.1" command: echo '--http1.1' >> ~/.curlrc build-docker-image: steps: @@ -41,36 +42,31 @@ commands: command: | docker build -t circleci/circleci-cli:0.1.$CIRCLE_BUILD_NUM-alpine --file Dockerfile.alpine . docker run --rm circleci/circleci-cli:0.1.$CIRCLE_BUILD_NUM-alpine update check - deploy-save-cache-workspace-and-artifacts: + deploy-save-workspace-and-artifacts: steps: - - save_cache: - key: v4-goreleaser-{{ checksum "~/goreleaser_amd64.deb" }} - paths: [~/goreleaser_amd64.deb] - persist_to_workspace: root: . paths: - - 'dist' + - "dist" - store_artifacts: path: ./dist destination: dist install-goreleaser: parameters: - GORELEASER_URL: + version: type: string - default: https://github.com/goreleaser/goreleaser/releases/download/v0.184.0/goreleaser_amd64.deb + default: "1.19.1" steps: - - restore_cache: - keys: [v5-goreleaser-] - run: name: Install GoReleaser command: | - [ -f ~/goreleaser_amd64.deb ] || curl --silent --location --fail --retry 3 << parameters.GORELEASER_URL >> > ~/goreleaser_amd64.deb - sudo apt-get update -y - sudo apt install ~/goreleaser_amd64.deb + echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list + sudo apt -q update -y + sudo apt -q install -y --no-install-recommends goreleaser=<< parameters.version >> gomod: steps: - restore_cache: - keys: ['v3-gomod-{{ arch }}-'] + keys: ["v3-gomod-{{ arch }}-"] - run: name: Download go module dependencies command: go mod download @@ -121,7 +117,7 @@ jobs: - persist_to_workspace: root: . paths: - - 'build' + - "build" cucumber: docker: - image: cimg/ruby:2.7 @@ -130,7 +126,7 @@ jobs: - attach_workspace: at: . - run: - name: 'Install CLI tool from workspace' + name: "Install CLI tool from workspace" command: sudo cp ~/project/build/linux/amd64/circleci /usr/local/bin/ - run: command: bundle install @@ -209,7 +205,7 @@ jobs: docker_layer_caching: true - build-docker-image - build-alpine-image - - deploy-save-cache-workspace-and-artifacts + - deploy-save-workspace-and-artifacts deploy: executor: go @@ -246,7 +242,7 @@ jobs: docker push circleci/circleci-cli:0.1.$CIRCLE_BUILD_NUM-alpine docker tag circleci/circleci-cli:0.1.$CIRCLE_BUILD_NUM-alpine circleci/circleci-cli:alpine docker push circleci/circleci-cli:alpine - - deploy-save-cache-workspace-and-artifacts + - deploy-save-workspace-and-artifacts snap: docker: diff --git a/Dockerfile b/Dockerfile index 7cdf842f2..88e84018c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,4 +4,4 @@ LABEL maintainer="Developer Experience Team " ENV CIRCLECI_CLI_SKIP_UPDATE_CHECK true -COPY ./dist/circleci-cli_linux_amd64/circleci /usr/local/bin +COPY ./dist/circleci-cli_linux_amd64_v1/circleci /usr/local/bin diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 71240212d..0983ed58d 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -2,7 +2,7 @@ FROM alpine:3.8 ENV CIRCLECI_CLI_SKIP_UPDATE_CHECK true -COPY ./dist/circleci-cli_linux_amd64/circleci /usr/local/bin +COPY ./dist/circleci-cli_linux_amd64_v1/circleci /usr/local/bin RUN apk add --no-cache --upgrade git openssh ca-certificates From d63d497bf0ba5a1fec9bc66c28d2c90bcfab55b3 Mon Sep 17 00:00:00 2001 From: Charles Francoise Date: Tue, 18 Jul 2023 09:45:25 +0200 Subject: [PATCH 05/36] deps: remove replace directive for etcd from go.mod (#966) * deps: remove replace directive for etcd from go.mod * add a few miising dependencies to go.sum --- go.mod | 3 --- go.sum | 42 ------------------------------------------ 2 files changed, 45 deletions(-) diff --git a/go.mod b/go.mod index 036ba1dd6..380836956 100644 --- a/go.mod +++ b/go.mod @@ -102,7 +102,4 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect ) -// fix vulnerability: CVE-2020-15114 in etcd v3.3.10+incompatible -replace github.com/coreos/etcd => github.com/coreos/etcd v3.3.24+incompatible - go 1.20 diff --git a/go.sum b/go.sum index c06fe807b..efca168b1 100644 --- a/go.sum +++ b/go.sum @@ -37,17 +37,12 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CircleCI-Public/circle-policy-agent v0.0.608 h1:3Bhimsdrhwiz2J7ssx90vVH5o8dviZb5+pWUzuMg9E0= github.com/CircleCI-Public/circle-policy-agent v0.0.608/go.mod h1:EJlgvMigkiPmhzRJrnqzucVtIYB0TlbNzaSAB2gEL9Q= -github.com/CircleCI-Public/circleci-config v0.0.0-20230609041455-d155ae7f0410 h1:xa+9peq4VEbHlIBOmhndOw+SmTDYyj9ZTHdp4rrzdOQ= -github.com/CircleCI-Public/circleci-config v0.0.0-20230609041455-d155ae7f0410/go.mod h1:XZaQPj2ylXZaz5vW31dRdkUY/Ey8MdpbgrUHbHyzICY= -github.com/CircleCI-Public/circleci-config v0.0.0-20230609135034-182164ce950a h1:RqA4H9p77FsqV++HNNDBq8dJftYuJ+r+KdD9HAX28t4= -github.com/CircleCI-Public/circleci-config v0.0.0-20230609135034-182164ce950a/go.mod h1:XZaQPj2ylXZaz5vW31dRdkUY/Ey8MdpbgrUHbHyzICY= github.com/CircleCI-Public/circleci-config v0.0.0-20230629192034-c469d9e9936b h1:emg7uU3bRjVMlwSpOATBiybaBPXNWUIiFE/qbQQXZtE= github.com/CircleCI-Public/circleci-config v0.0.0-20230629192034-c469d9e9936b/go.mod h1:0iW5+XMF4XtikTlfCElaBQjT/OTMjQRHM1DgSWxcWuE= github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= @@ -59,26 +54,18 @@ github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= -github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/charmbracelet/bubbles v0.11.0 h1:fBLyY0PvJnd56Vlu5L84JJH6f4axhgIJ9P3NET78f0Q= github.com/charmbracelet/bubbles v0.11.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= github.com/charmbracelet/bubbletea v0.21.0 h1:f3y+kanzgev5PA916qxmDybSHU3N804uOnKnhRPXTcI= @@ -101,12 +88,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -118,20 +100,16 @@ github.com/erikgeiser/promptkit v0.7.0/go.mod h1:Jj9bhN+N8RbMjB1jthkr9A4ydmczZ1W github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -140,9 +118,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -172,10 +148,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -209,7 +183,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= -github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -228,20 +201,15 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -258,10 +226,8 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= @@ -301,11 +267,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rhysd/go-github-selfupdate v0.0.0-20180520142321-41c1bbb0804a h1:YNh/SV+Z0p7kQDUE9Ux+46ruTucvQP43XB06DfZa8Es= @@ -314,7 +276,6 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= @@ -322,7 +283,6 @@ github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= @@ -366,7 +326,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -672,7 +631,6 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= From 18dc782df73e52f46bf6c93959790fdb5fdfb009 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Wed, 19 Jul 2023 11:48:41 +0200 Subject: [PATCH 06/36] docs: Improved the compatibility matrix in the README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a955cff70..6f0be8430 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ 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 |
  • `circleci config validate`
  • `circleci config process`
  • `circleci local execute`
  • | The config validation has been moved from the GraphQL API to a specific API endpoint |
    • **Server v4.0.5, v4.1.3, v4.2.0 and above**: Commands use the new specific endpoint
    • **Previous version**: Commands use the GraphQL API
    | +| Orb compilation and validation of orb using private orbs |
    • `circleci orb process`
    • `circleci orb validate`
    | 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 |
    • **Server v4.2.0 and above**: The field is accessible so you can use the parameter
    • **Previous versions**: The field does not exist making the functionality unavailable
    | \ No newline at end of file From 1ecfa71bcbe293993dc51d5f26668c96b8f751a4 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 27 Jun 2023 09:55:21 +0200 Subject: [PATCH 07/36] feat: Created telemetry client --- api/api.go | 19 +++++++ clitest/clitest.go | 2 + cmd/root.go | 2 + cmd/telemetry.go | 111 ++++++++++++++++++++++++++++++++++++ go.mod | 4 ++ go.sum | 8 +++ prompt/prompt.go | 11 ++++ settings/settings.go | 77 +++++++++++++++++++++++++ telemetry/telemetry.go | 126 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 360 insertions(+) create mode 100644 cmd/telemetry.go create mode 100644 telemetry/telemetry.go diff --git a/api/api.go b/api/api.go index 0c51b486d..a98be1a94 100644 --- a/api/api.go +++ b/api/api.go @@ -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" @@ -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 +} diff --git a/clitest/clitest.go b/clitest/clitest.go index 6a0010d5c..62e83180b 100644 --- a/clitest/clitest.go +++ b/clitest/clitest.go @@ -33,6 +33,7 @@ type TempSettings struct { Home string TestServer *ghttp.Server Config *TmpFile + Telemetry *TmpFile Update *TmpFile } @@ -68,6 +69,7 @@ func WithTempSettings() *TempSettings { gomega.Expect(os.Mkdir(settingsPath, 0700)).To(gomega.Succeed()) tempSettings.Config = OpenTmpFile(settingsPath, "cli.yml") + tempSettings.Telemetry = OpenTmpFile(settingsPath, "telemetry.yml") tempSettings.Update = OpenTmpFile(settingsPath, "update_check.yml") tempSettings.TestServer = ghttp.NewServer() diff --git a/cmd/root.go b/cmd/root.go index 942278c69..7aa1d16c1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -184,6 +184,7 @@ func MakeCommands() *cobra.Command { flags.StringVar(&rootOptions.Endpoint, "endpoint", rootOptions.Endpoint, "URI to your CircleCI GraphQL API endpoint") 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.Telemetry.DisabledFromParams, "disable-telemetry", false, "Do not show telemetry for the actual command") hidden := []string{"github-api", "debug", "endpoint"} @@ -227,6 +228,7 @@ func rootCmdPreRun(rootOptions *settings.Config) error { fmt.Printf("Error checking for updates: %s\n", err) fmt.Printf("Please contact support.\n\n") } + return nil } diff --git a/cmd/telemetry.go b/cmd/telemetry.go new file mode 100644 index 000000000..29f135739 --- /dev/null +++ b/cmd/telemetry.go @@ -0,0 +1,111 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/rest" + "github.com/CircleCI-Public/circleci-cli/prompt" + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/google/uuid" + "github.com/pkg/errors" + "golang.org/x/term" +) + +const ( + TelemetryIsActive = "yes" + TelemetryIsInactive = "no" + + // This value means telemetry is disabled because we have no access to stdin and so can't ask user approval + TelemetryDefaultDisabled = "default-disabled" +) + +type telemetryUI interface { + AskUserToApproveTelemetry(message string) bool +} + +type telemetryInteractiveUI struct{} + +func (telemetryInteractiveUI) AskUserToApproveTelemetry(message string) bool { + return prompt.AskUserToConfirmWithDefault(message, true) +} + +type telemetryTestUI struct { + Approved bool +} + +func (ui telemetryTestUI) AskUserToApproveTelemetry(message string) bool { + return ui.Approved +} + +// Make sure the user gave their approval for the telemetry and +func checkTelemetry(config *settings.Config, ui telemetryUI) error { + config.Telemetry.Load() + + if err := askForTelemetryApproval(config, ui); err != nil { + config.Telemetry.Client = telemetry.CreateClient(telemetry.User{}, false) + return err + } + + config.Telemetry.Client = telemetry.CreateClient(telemetry.User{ + UniqueID: config.Telemetry.UniqueID, + UserID: config.Telemetry.UserID, + }, config.Telemetry.IsActive) + return nil +} + +func askForTelemetryApproval(config *settings.Config, ui telemetryUI) error { + // If we already have telemetry information or that telemetry is explicitly disabled, skip + if config.Telemetry.HasAnsweredPrompt || config.Telemetry.DisabledFromParams { + return nil + } + + // If stdin is not available, send telemetry event, disactive telemetry and return + if !term.IsTerminal(int(os.Stdin.Fd())) { + telemetry.SendTelemetryApproval(telemetry.User{}, telemetry.NoStdin) + config.Telemetry.IsActive = false + return nil + } + + // Else ask user for telemetry approval + fmt.Println("CircleCI would like to collect CLI usage data for product improvement purposes.") + fmt.Println("") + fmt.Println("Participation is voluntary, and your choice can be changed at any time through the command `cli telemetry enable` and `cli telemetry disable`.") + fmt.Println("For more information, please see our privacy policy at https://circleci.com/legal/privacy/.") + fmt.Println("") + config.Telemetry.IsActive = ui.AskUserToApproveTelemetry("Enable telemetry?") + config.Telemetry.HasAnsweredPrompt = true + + // If user allows telemetry, create a telemetry user + user := telemetry.User{} + if config.Telemetry.UniqueID == "" { + user.UniqueID = uuid.New().String() + } + + if config.Telemetry.IsActive && config.Token != "" { + me, err := api.GetMe(rest.NewFromConfig(config.Host, config)) + if err != nil { + user.UserID = me.ID + } + } + config.Telemetry.UniqueID = user.UniqueID + config.Telemetry.UserID = user.UserID + + // Send telemetry approval event + approval := telemetry.Enabled + if !config.Telemetry.IsActive { + approval = telemetry.Disabled + } + if err := telemetry.SendTelemetryApproval(telemetry.User{}, approval); err != nil { + return err + } + + // Write telemetry + if err := config.Telemetry.Write(); err != nil { + return errors.Wrap(err, "Writing telemetry to disk") + } + + return nil +} diff --git a/go.mod b/go.mod index f305f8565..4fd9ee2bc 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/charmbracelet/lipgloss v0.5.0 github.com/erikgeiser/promptkit v0.7.0 github.com/hexops/gotextdiff v1.0.3 + github.com/segmentio/analytics-go v3.1.0+incompatible github.com/stretchr/testify v1.8.3 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 golang.org/x/term v0.10.0 @@ -50,6 +51,7 @@ require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/charmbracelet/bubbles v0.11.0 // indirect github.com/charmbracelet/bubbletea v0.21.0 // indirect @@ -94,6 +96,7 @@ require ( github.com/prometheus/procfs v0.11.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/segmentio/backo-go v1.0.1 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.1.1 // indirect @@ -101,6 +104,7 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect go.opentelemetry.io/otel v1.16.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect diff --git a/go.sum b/go.sum index 7ab560a5e..e7747af1e 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -241,6 +243,10 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= +github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80= +github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= +github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4= +github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -273,6 +279,8 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMc github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/prompt/prompt.go b/prompt/prompt.go index cb2bd52dd..c9613386c 100644 --- a/prompt/prompt.go +++ b/prompt/prompt.go @@ -36,3 +36,14 @@ func AskUserToConfirm(message string) bool { result, err := input.RunPrompt() return err == nil && result } + +func AskUserToConfirmWithDefault(message string, defaultValue bool) bool { + def := confirmation.No + if defaultValue { + def = confirmation.Yes + } + + input := confirmation.New(message, def) + result, err := input.RunPrompt() + return err == nil && result +} diff --git a/settings/settings.go b/settings/settings.go index 36928fac5..b77b4f9a2 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -17,6 +17,8 @@ import ( yaml "gopkg.in/yaml.v3" "github.com/CircleCI-Public/circleci-cli/data" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/CircleCI-Public/circleci-cli/version" ) // Config is used to represent the current state of a CLI instance. @@ -36,6 +38,7 @@ type Config struct { GitHubAPI string `yaml:"-"` SkipUpdateCheck bool `yaml:"-"` OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"` + Telemetry TelemetrySettings `yaml:"-"` } type OrbPublishingInfo struct { @@ -50,6 +53,17 @@ type UpdateCheck struct { FileUsed string `yaml:"-"` } +// TelemetrySettings is used to represent telemetry related settings +type TelemetrySettings struct { + IsActive bool `yaml:"is_active"` + HasAnsweredPrompt bool `yaml:"has_answered_prompt"` + DisabledFromParams bool `yaml:"-"` + UniqueID string `yaml:"unique_id"` + UserID string `yaml:"user_id"` + + Client telemetry.Client `yaml:"-"` +} + // Load will read the update check settings from the user's disk and then deserialize it into the current instance. func (upd *UpdateCheck) Load() error { path := filepath.Join(SettingsPath(), updateCheckFilename()) @@ -80,6 +94,64 @@ func (upd *UpdateCheck) WriteToDisk() error { return err } +// Load will read the telemetry settings from the user's disk and then deserialize it into the current instance. +func (tel *TelemetrySettings) Load() error { + path := filepath.Join(SettingsPath(), telemetryFilename()) + + if err := ensureSettingsFileExists(path); err != nil { + return err + } + + content, err := os.ReadFile(path) // #nosec + if err != nil { + return err + } + + err = yaml.Unmarshal(content, &tel) + return err +} + +// WriteToDisk will write the telemetry settings to disk by serializing the YAML +func (tel *TelemetrySettings) Write() error { + enc, err := yaml.Marshal(&tel) + if err != nil { + return err + } + + path := filepath.Join(SettingsPath(), telemetryFilename()) + err = os.WriteFile(path, enc, 0600) + return err +} + +// Track takes a telemetry event, enrich with various data and sends it +// This is the method you must use to send telemetry events +// This will fail if 'checkTelemetry' has not called before +func (cfg *Config) Track(event telemetry.Event) error { + if cfg.Telemetry.Client == nil { + return errors.New("No telemetry client found") + } + + if cfg.Telemetry.UniqueID != "" { + event.Properties["UUID"] = cfg.Telemetry.UniqueID + } + + if cfg.Telemetry.UserID != "" { + event.Properties["user_id"] = cfg.Telemetry.UserID + } + + if cfg.Host != "" { + event.Properties["host"] = cfg.Host + } else { + event.Properties["host"] = "https://circleci.com" + } + + event.Properties["os"] = runtime.GOOS + event.Properties["cli_version"] = version.Version + event.Properties["team_name"] = "devex" + + return cfg.Telemetry.Client.Track(event) +} + // Load will read the config from the user's disk and then evaluate possible configuration from the environment. func (cfg *Config) Load() error { if err := cfg.LoadFromDisk(); err != nil { @@ -161,6 +233,11 @@ func configFilename() string { return "cli.yml" } +// telemetryFilename returns the name of the cli telemetry file +func telemetryFilename() string { + return "telemetry.yml" +} + // settingsPath returns the path of the CLI settings directory func SettingsPath() string { // TODO: Make this configurable diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go new file mode 100644 index 000000000..7a7812e23 --- /dev/null +++ b/telemetry/telemetry.go @@ -0,0 +1,126 @@ +package telemetry + +import ( + "fmt" + "io" + + "github.com/segmentio/analytics-go" +) + +type Approval string + +const ( + Enabled Approval = "enabled" + Disabled Approval = "disabled" + NoStdin Approval = "disabled_default" +) + +type Client interface { + io.Closer + // Send a telemetry event. This method is not to be called directly. Use config.Track instead + Track(event Event) error +} + +type Event struct { + Object string + Action string + Properties map[string]interface{} +} + +type User struct { + UniqueID string + + UserID string +} + +// Create a telemetry client to be used to send telemetry event +func CreateClient(user User, enabled bool) Client { + if !enabled { + return nullClient{} + } + + client := newSegmentClient() + if err := client.identify(user); err != nil { + return nullClient{} + } + + return client +} + +// Sends the user's approval event +func SendTelemetryApproval(user User, approval Approval) error { + client := newSegmentClient() + + if approval == Enabled { + if err := client.identify(user); err != nil { + return err + } + } + + return client.Track(Event{ + Object: "cli-telemetry", + Action: string(approval), + }) +} + +// Null client +// Used when telemetry is disabled + +type nullClient struct{} + +func (cli nullClient) Close() error { return nil } + +func (cli nullClient) Track(_ Event) error { return nil } + +// Log telemetry +// Used for tests + +type logClient struct{} + +func (cli logClient) Close() error { return nil } + +func (cli logClient) identify(_ User) error { return nil } + +func (cli logClient) Track(e Event) error { + fmt.Printf("\n*** Telemetry event ***\nObject: %s\nAction: %s\nProperties: %+v\n\n", e.Object, e.Action, e.Properties) + return nil +} + +// Segment client +// Used when telemetry is enabled + +type segmentClient struct { + cli analytics.Client +} + +func newSegmentClient() logClient { + return logClient{} + // return &segmentClient{ + // cli: analytics.New(""), + // } +} + +func (segment *segmentClient) identify(user User) error { + traits := analytics.NewTraits().Set("UUID", user.UniqueID) + + if user.UserID != "" { + traits = traits.Set("userId", user.UserID) + } + return segment.cli.Enqueue( + analytics.Identify{ + UserId: user.UniqueID, + }, + ) +} + +func (segment *segmentClient) Track(event Event) error { + event.Properties["action"] = event.Action + return segment.cli.Enqueue(analytics.Track{ + Event: event.Object, + Properties: event.Properties, + }) +} + +func (segment *segmentClient) Close() error { + return segment.cli.Close() +} From cb04a0b0d2d50ab77f53e5c64e79261abb8d27d8 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 27 Jun 2023 17:55:48 +0200 Subject: [PATCH 08/36] test: Written unit test for telemetry --- cmd/{telemetry.go => check_telemetry.go} | 26 +- cmd/check_telemetry_test.go | 203 +++++++++++++ cmd/root.go | 5 + go.mod | 1 + go.sum | 369 +++++++++++++++++++++++ settings/settings.go | 11 +- telemetry/telemetry.go | 67 ++-- 7 files changed, 628 insertions(+), 54 deletions(-) rename cmd/{telemetry.go => check_telemetry.go} (84%) create mode 100644 cmd/check_telemetry_test.go diff --git a/cmd/telemetry.go b/cmd/check_telemetry.go similarity index 84% rename from cmd/telemetry.go rename to cmd/check_telemetry.go index 29f135739..830848148 100644 --- a/cmd/telemetry.go +++ b/cmd/check_telemetry.go @@ -14,12 +14,9 @@ import ( "golang.org/x/term" ) -const ( - TelemetryIsActive = "yes" - TelemetryIsInactive = "no" - - // This value means telemetry is disabled because we have no access to stdin and so can't ask user approval - TelemetryDefaultDisabled = "default-disabled" +var ( + createUUID = func() string { return uuid.New().String() } + isStdinOpen = term.IsTerminal(int(os.Stdin.Fd())) ) type telemetryUI interface { @@ -63,10 +60,11 @@ func askForTelemetryApproval(config *settings.Config, ui telemetryUI) error { } // If stdin is not available, send telemetry event, disactive telemetry and return - if !term.IsTerminal(int(os.Stdin.Fd())) { - telemetry.SendTelemetryApproval(telemetry.User{}, telemetry.NoStdin) + if !isStdinOpen { config.Telemetry.IsActive = false - return nil + return telemetry.SendTelemetryApproval(telemetry.User{ + UniqueID: config.Telemetry.UniqueID, + }, telemetry.NoStdin) } // Else ask user for telemetry approval @@ -79,14 +77,16 @@ func askForTelemetryApproval(config *settings.Config, ui telemetryUI) error { config.Telemetry.HasAnsweredPrompt = true // If user allows telemetry, create a telemetry user - user := telemetry.User{} + user := telemetry.User{ + UniqueID: config.Telemetry.UniqueID, + } if config.Telemetry.UniqueID == "" { - user.UniqueID = uuid.New().String() + user.UniqueID = createUUID() } if config.Telemetry.IsActive && config.Token != "" { me, err := api.GetMe(rest.NewFromConfig(config.Host, config)) - if err != nil { + if err == nil { user.UserID = me.ID } } @@ -98,7 +98,7 @@ func askForTelemetryApproval(config *settings.Config, ui telemetryUI) error { if !config.Telemetry.IsActive { approval = telemetry.Disabled } - if err := telemetry.SendTelemetryApproval(telemetry.User{}, approval); err != nil { + if err := telemetry.SendTelemetryApproval(user, approval); err != nil { return err } diff --git a/cmd/check_telemetry_test.go b/cmd/check_telemetry_test.go new file mode 100644 index 000000000..c0e994552 --- /dev/null +++ b/cmd/check_telemetry_test.go @@ -0,0 +1,203 @@ +package cmd + +import ( + "fmt" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/spf13/afero" + "gotest.tools/v3/assert" +) + +type testTelemetry struct { + events []telemetry.Event +} + +func (cli *testTelemetry) Close() error { return nil } + +func (cli *testTelemetry) Track(event telemetry.Event) error { + cli.events = append(cli.events, event) + return nil +} + +func TestAskForTelemetryApproval(t *testing.T) { + // Mock HTTP + userId := "id" + uniqueId := "unique-id" + response := fmt.Sprintf(`{"id":"%s","login":"login","name":"name"}`, userId) + var handler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET") + assert.Equal(t, r.URL.String(), "/me") + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(response)) + } + server := httptest.NewServer(handler) + defer server.Close() + + // Mock create UUID + oldUUIDCreate := createUUID + createUUID = func() string { return uniqueId } + defer (func() { createUUID = oldUUIDCreate })() + + // Create test cases + type args struct { + closeStdin bool + promptApproval bool + config settings.TelemetrySettings + } + type want struct { + config settings.TelemetrySettings + fileNotCreated bool + telemetryEvents []telemetry.Event + } + type testCase struct { + name string + args args + want want + } + + testCases := []testCase{ + { + name: "Prompt approval should be saved in settings", + args: args{ + promptApproval: true, + config: settings.TelemetrySettings{}, + }, + want: want{ + config: settings.TelemetrySettings{ + IsActive: true, + HasAnsweredPrompt: true, + UserID: userId, + UniqueID: uniqueId, + }, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "enabled"}, + }, + }, + }, + { + name: "Prompt disapproval should be saved in settings", + args: args{ + promptApproval: false, + config: settings.TelemetrySettings{}, + }, + want: want{ + config: settings.TelemetrySettings{ + IsActive: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + }, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "disabled"}, + }, + }, + }, + { + name: "Does not recreate a unique ID if there is one", + args: args{ + promptApproval: true, + config: settings.TelemetrySettings{ + UniqueID: "other-id", + }, + }, + want: want{ + config: settings.TelemetrySettings{ + IsActive: true, + HasAnsweredPrompt: true, + UserID: userId, + UniqueID: "other-id", + }, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "enabled"}, + }, + }, + }, + { + name: "Does not change telemetry settings if user already answered prompt", + args: args{ + config: settings.TelemetrySettings{ + HasAnsweredPrompt: true, + }, + }, + want: want{ + fileNotCreated: true, + telemetryEvents: []telemetry.Event{}, + }, + }, + { + name: "Does not change telemetry settings if user disabled telemetry", + args: args{ + config: settings.TelemetrySettings{ + DisabledFromParams: true, + }, + }, + want: want{ + fileNotCreated: true, + telemetryEvents: []telemetry.Event{}, + }, + }, + { + name: "Does not change telemetry settings if stdin is not open", + args: args{closeStdin: true}, + want: want{ + fileNotCreated: true, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "disabled_default"}, + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + // Mock FS + oldFS := settings.FS.Fs + settings.FS.Fs = afero.NewMemMapFs() + defer (func() { settings.FS.Fs = oldFS })() + + // Mock stdin + oldIsStdinOpen := isStdinOpen + isStdinOpen = !tt.args.closeStdin + defer (func() { isStdinOpen = oldIsStdinOpen })() + + // Mock telemetry + telemetryClient := testTelemetry{events: make([]telemetry.Event, 0)} + oldCreateActiveTelemetry := telemetry.CreateActiveTelemetry + telemetry.CreateActiveTelemetry = func(_ telemetry.User) telemetry.Client { + return &telemetryClient + } + defer (func() { telemetry.CreateActiveTelemetry = oldCreateActiveTelemetry })() + + // Run askForTelemetryApproval + config := settings.Config{ + Token: "testtoken", + HTTPClient: http.DefaultClient, + Host: server.URL, + Telemetry: tt.args.config, + } + err := askForTelemetryApproval(&config, telemetryTestUI{tt.args.promptApproval}) + assert.NilError(t, err) + + // Verify good telemetry events were sent + assert.DeepEqual(t, telemetryClient.events, tt.want.telemetryEvents) + + // Verify if settings file exist + exist, err := settings.FS.Exists(filepath.Join(settings.SettingsPath(), "telemetry.yml")) + assert.NilError(t, err) + assert.Equal(t, exist, !tt.want.fileNotCreated) + if tt.want.fileNotCreated { + return + } + + // Verify settings file content + result := settings.TelemetrySettings{} + err = result.Load() + assert.NilError(t, err) + assert.Equal(t, result, tt.want.config) + }) + } +} diff --git a/cmd/root.go b/cmd/root.go index 7aa1d16c1..6288231f2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -126,6 +126,11 @@ func MakeCommands() *cobra.Command { PersistentPreRunE: func(_ *cobra.Command, _ []string) error { return rootCmdPreRun(rootOptions) }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if rootOptions.Telemetry.Client != nil { + rootOptions.Telemetry.Client.Close() + } + }, } // For supporting "Args" in command usage help diff --git a/go.mod b/go.mod index 4fd9ee2bc..710386a9d 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/erikgeiser/promptkit v0.7.0 github.com/hexops/gotextdiff v1.0.3 github.com/segmentio/analytics-go v3.1.0+incompatible + github.com/spf13/afero v1.9.5 github.com/stretchr/testify v1.8.3 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 golang.org/x/term v0.10.0 diff --git a/go.sum b/go.sum index e7747af1e..4d5d49640 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,45 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AlecAivazis/survey/v2 v2.1.1 h1:LEMbHE0pLj75faaVEKClEX1TM4AJmmnOh9eimREzLWI= github.com/AlecAivazis/survey/v2 v2.1.1/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CircleCI-Public/circle-policy-agent v0.0.683 h1:EzZaLy9mUGl4dwDNWceBHeDb3X0KAAjV4eFOk3C7lts= github.com/CircleCI-Public/circle-policy-agent v0.0.683/go.mod h1:72U4Q4OtvAGRGGo/GqlCCO0tARg1cSG9xwxWyz3ktQI= github.com/CircleCI-Public/circleci-config v0.0.0-20230629192034-c469d9e9936b h1:emg7uU3bRjVMlwSpOATBiybaBPXNWUIiFE/qbQQXZtE= @@ -39,6 +79,7 @@ github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/pp github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -49,9 +90,16 @@ github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -67,6 +115,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikgeiser/promptkit v0.7.0 h1:Yi28iN6JRs8/0x+wjQRPfWb+vWz1pFmZ5fu2uoFipD8= github.com/erikgeiser/promptkit v0.7.0/go.mod h1:Jj9bhN+N8RbMjB1jthkr9A4ydmczZ1WZJ8xTXnP12dg= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -89,6 +143,9 @@ github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw4 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -100,26 +157,51 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -127,17 +209,39 @@ github.com/google/go-github v15.0.0+incompatible h1:jlPg2Cpsxb/FyEV/MFiIE9tW/2RA github.com/google/go-github v15.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= @@ -146,11 +250,15 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -222,10 +330,12 @@ github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= @@ -239,6 +349,7 @@ github.com/rhysd/go-github-selfupdate v0.0.0-20180520142321-41c1bbb0804a/go.mod github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= @@ -254,6 +365,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -283,8 +396,17 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM= go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= @@ -300,26 +422,87 @@ go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZE go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -328,31 +511,73 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -376,46 +601,180 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20230724170836-66ad5b6ff146 h1:0yJBPCSj/Hy/vQsNSrYtRLuJSNKoDzDXMu1q1ePGdus= google.golang.org/genproto v0.0.0-20230724170836-66ad5b6ff146/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108= google.golang.org/genproto/googleapis/api v0.0.0-20230724170836-66ad5b6ff146 h1:P60zJj7Yxq1VhZIxpRO7A5lDFyy07D6Dqa+HCixuFBM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230724170836-66ad5b6ff146 h1:0PjALPu/U/4OVXKQM2P8b8NJGd4V+xbZSP+uuBJpGm0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -431,3 +790,13 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/settings/settings.go b/settings/settings.go index b77b4f9a2..89f27df36 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -19,6 +19,13 @@ import ( "github.com/CircleCI-Public/circleci-cli/data" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/version" + "github.com/spf13/afero" +) + +var ( + FS afero.Afero = afero.Afero{ + Fs: afero.NewOsFs(), + } ) // Config is used to represent the current state of a CLI instance. @@ -102,7 +109,7 @@ func (tel *TelemetrySettings) Load() error { return err } - content, err := os.ReadFile(path) // #nosec + content, err := FS.ReadFile(path) // #nosec if err != nil { return err } @@ -119,7 +126,7 @@ func (tel *TelemetrySettings) Write() error { } path := filepath.Join(SettingsPath(), telemetryFilename()) - err = os.WriteFile(path, enc, 0600) + err = FS.WriteFile(path, enc, 0600) return err } diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 7a7812e23..7bda42412 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -1,7 +1,6 @@ package telemetry import ( - "fmt" "io" "github.com/segmentio/analytics-go" @@ -9,6 +8,11 @@ import ( type Approval string +var ( + // Overwrite this function for tests + CreateActiveTelemetry = newSegmentClient +) + const ( Enabled Approval = "enabled" Disabled Approval = "disabled" @@ -39,23 +43,12 @@ func CreateClient(user User, enabled bool) Client { return nullClient{} } - client := newSegmentClient() - if err := client.identify(user); err != nil { - return nullClient{} - } - - return client + return CreateActiveTelemetry(user) } // Sends the user's approval event func SendTelemetryApproval(user User, approval Approval) error { - client := newSegmentClient() - - if approval == Enabled { - if err := client.identify(user); err != nil { - return err - } - } + client := CreateActiveTelemetry(user) return client.Track(Event{ Object: "cli-telemetry", @@ -72,50 +65,46 @@ func (cli nullClient) Close() error { return nil } func (cli nullClient) Track(_ Event) error { return nil } -// Log telemetry -// Used for tests - -type logClient struct{} - -func (cli logClient) Close() error { return nil } - -func (cli logClient) identify(_ User) error { return nil } - -func (cli logClient) Track(e Event) error { - fmt.Printf("\n*** Telemetry event ***\nObject: %s\nAction: %s\nProperties: %+v\n\n", e.Object, e.Action, e.Properties) - return nil -} - // Segment client // Used when telemetry is enabled type segmentClient struct { - cli analytics.Client + cli analytics.Client + userId string } -func newSegmentClient() logClient { - return logClient{} - // return &segmentClient{ - // cli: analytics.New(""), - // } -} +const ( + segmentKey = "" +) -func (segment *segmentClient) identify(user User) error { - traits := analytics.NewTraits().Set("UUID", user.UniqueID) +func newSegmentClient(user User) Client { + cli := analytics.New(segmentKey) + userID := user.UniqueID + if userID == "" { + userID = "none" + } + + traits := analytics.NewTraits().Set("UUID", user.UniqueID) if user.UserID != "" { traits = traits.Set("userId", user.UserID) } - return segment.cli.Enqueue( + cli.Enqueue( analytics.Identify{ - UserId: user.UniqueID, + UserId: userID, }, ) + + return &segmentClient{cli, userID} } func (segment *segmentClient) Track(event Event) error { + if event.Properties == nil { + event.Properties = make(map[string]interface{}) + } event.Properties["action"] = event.Action return segment.cli.Enqueue(analytics.Track{ + UserId: segment.userId, Event: event.Object, Properties: event.Properties, }) From a68d639a581e4da4e1d2a9e2db4bf2840cbc7cc9 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Wed, 5 Jul 2023 10:08:05 +0200 Subject: [PATCH 09/36] style: PR comments --- cmd/check_telemetry.go | 111 ------------- cmd/create_telemetry.go | 155 ++++++++++++++++++ ...metry_test.go => create_telemetry_test.go} | 142 +++++++++------- cmd/root.go | 7 +- settings/settings.go | 74 +++------ telemetry/telemetry.go | 51 ++++-- 6 files changed, 301 insertions(+), 239 deletions(-) delete mode 100644 cmd/check_telemetry.go create mode 100644 cmd/create_telemetry.go rename cmd/{check_telemetry_test.go => create_telemetry_test.go} (54%) diff --git a/cmd/check_telemetry.go b/cmd/check_telemetry.go deleted file mode 100644 index 830848148..000000000 --- a/cmd/check_telemetry.go +++ /dev/null @@ -1,111 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/CircleCI-Public/circleci-cli/api" - "github.com/CircleCI-Public/circleci-cli/api/rest" - "github.com/CircleCI-Public/circleci-cli/prompt" - "github.com/CircleCI-Public/circleci-cli/settings" - "github.com/CircleCI-Public/circleci-cli/telemetry" - "github.com/google/uuid" - "github.com/pkg/errors" - "golang.org/x/term" -) - -var ( - createUUID = func() string { return uuid.New().String() } - isStdinOpen = term.IsTerminal(int(os.Stdin.Fd())) -) - -type telemetryUI interface { - AskUserToApproveTelemetry(message string) bool -} - -type telemetryInteractiveUI struct{} - -func (telemetryInteractiveUI) AskUserToApproveTelemetry(message string) bool { - return prompt.AskUserToConfirmWithDefault(message, true) -} - -type telemetryTestUI struct { - Approved bool -} - -func (ui telemetryTestUI) AskUserToApproveTelemetry(message string) bool { - return ui.Approved -} - -// Make sure the user gave their approval for the telemetry and -func checkTelemetry(config *settings.Config, ui telemetryUI) error { - config.Telemetry.Load() - - if err := askForTelemetryApproval(config, ui); err != nil { - config.Telemetry.Client = telemetry.CreateClient(telemetry.User{}, false) - return err - } - - config.Telemetry.Client = telemetry.CreateClient(telemetry.User{ - UniqueID: config.Telemetry.UniqueID, - UserID: config.Telemetry.UserID, - }, config.Telemetry.IsActive) - return nil -} - -func askForTelemetryApproval(config *settings.Config, ui telemetryUI) error { - // If we already have telemetry information or that telemetry is explicitly disabled, skip - if config.Telemetry.HasAnsweredPrompt || config.Telemetry.DisabledFromParams { - return nil - } - - // If stdin is not available, send telemetry event, disactive telemetry and return - if !isStdinOpen { - config.Telemetry.IsActive = false - return telemetry.SendTelemetryApproval(telemetry.User{ - UniqueID: config.Telemetry.UniqueID, - }, telemetry.NoStdin) - } - - // Else ask user for telemetry approval - fmt.Println("CircleCI would like to collect CLI usage data for product improvement purposes.") - fmt.Println("") - fmt.Println("Participation is voluntary, and your choice can be changed at any time through the command `cli telemetry enable` and `cli telemetry disable`.") - fmt.Println("For more information, please see our privacy policy at https://circleci.com/legal/privacy/.") - fmt.Println("") - config.Telemetry.IsActive = ui.AskUserToApproveTelemetry("Enable telemetry?") - config.Telemetry.HasAnsweredPrompt = true - - // If user allows telemetry, create a telemetry user - user := telemetry.User{ - UniqueID: config.Telemetry.UniqueID, - } - if config.Telemetry.UniqueID == "" { - user.UniqueID = createUUID() - } - - if config.Telemetry.IsActive && config.Token != "" { - me, err := api.GetMe(rest.NewFromConfig(config.Host, config)) - if err == nil { - user.UserID = me.ID - } - } - config.Telemetry.UniqueID = user.UniqueID - config.Telemetry.UserID = user.UserID - - // Send telemetry approval event - approval := telemetry.Enabled - if !config.Telemetry.IsActive { - approval = telemetry.Disabled - } - if err := telemetry.SendTelemetryApproval(user, approval); err != nil { - return err - } - - // Write telemetry - if err := config.Telemetry.Write(); err != nil { - return errors.Wrap(err, "Writing telemetry to disk") - } - - return nil -} diff --git a/cmd/create_telemetry.go b/cmd/create_telemetry.go new file mode 100644 index 000000000..95e54105d --- /dev/null +++ b/cmd/create_telemetry.go @@ -0,0 +1,155 @@ +package cmd + +import ( + "fmt" + "os" + "runtime" + + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/rest" + "github.com/CircleCI-Public/circleci-cli/prompt" + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/CircleCI-Public/circleci-cli/version" + "github.com/google/uuid" + "golang.org/x/term" +) + +var ( + createUUID = func() string { return uuid.New().String() } + isStdinATTY = term.IsTerminal(int(os.Stdin.Fd())) +) + +type telemetryUI interface { + AskUserToApproveTelemetry(message string) bool +} + +type telemetryInteractiveUI struct{} + +func (telemetryInteractiveUI) AskUserToApproveTelemetry(message string) bool { + return prompt.AskUserToConfirmWithDefault(message, true) +} + +type telemetryAPIClient interface { + getMyUserId() (string, error) +} + +type telemetryCircleCIAPI struct { + cli *rest.Client +} + +func (client telemetryCircleCIAPI) getMyUserId() (string, error) { + me, err := api.GetMe(client.cli) + if err != nil { + return "", err + } + return me.ID, nil +} + +// Make sure the user gave their approval for the telemetry and +func createTelemetry(config *settings.Config) telemetry.Client { + if config.IsTelemetryDisabled { + return telemetry.CreateClient(telemetry.User{}, false) + } + + apiClient := telemetryCircleCIAPI{ + cli: rest.NewFromConfig(config.Host, config), + } + ui := telemetryInteractiveUI{} + + telemetrySettings := settings.TelemetrySettings{} + user := telemetry.User{ + IsSelfHosted: config.Host == defaultHost, + OS: runtime.GOOS, + Version: version.Version, + TeamName: "devex", + } + + loadTelemetrySettings(&telemetrySettings, &user, apiClient, ui) + client := telemetry.CreateClient(user, telemetrySettings.IsActive) + + return client +} + +func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry.User, apiClient telemetryAPIClient, ui telemetryUI) { + err := settings.Load() + if err != nil && !os.IsNotExist(err) { + fmt.Printf("Error loading telemetry configuration: %s\n", err) + } + + user.UniqueID = settings.UniqueID + user.UserID = settings.UserID + + // If we already have telemetry information or that telemetry is explicitly disabled, skip + if settings.HasAnsweredPrompt { + // If we have no user id, we try requesting the user id again + if settings.UserID == "" && settings.IsActive { + myID, err := apiClient.getMyUserId() + if err == nil { + settings.UserID = myID + user.UserID = myID + if err := settings.Write(); err != nil { + fmt.Printf("Error writing telemetry settings to disk: %s\n", err) + } + } + } + + return + } + + // If stdin is not available, send telemetry event, disable telemetry and return + if !isStdinATTY { + settings.IsActive = false + err := telemetry.SendTelemetryApproval(telemetry.User{ + UniqueID: settings.UniqueID, + }, telemetry.NoStdin) + if err != nil { + fmt.Printf("Error while sending telemetry approval %s\n", err) + } + return + } + + // Else ask user for telemetry approval + fmt.Println("CircleCI would like to collect CLI usage data for product improvement purposes.") + fmt.Println("") + fmt.Println("Participation is voluntary, and your choice can be changed at any time through the command `cli telemetry enable` and `cli telemetry disable`.") + fmt.Println("For more information, please see our privacy policy at https://circleci.com/legal/privacy/.") + fmt.Println("") + settings.IsActive = ui.AskUserToApproveTelemetry("Enable telemetry?") + settings.HasAnsweredPrompt = true + + // Make sure we have user info and set them + if settings.IsActive { + if settings.UniqueID == "" { + settings.UniqueID = createUUID() + } + user.UniqueID = settings.UniqueID + + if settings.UserID == "" { + myID, err := apiClient.getMyUserId() + if err == nil { + settings.UserID = myID + } + } + user.UserID = settings.UserID + } else { + *user = telemetry.User{} + } + + // Send telemetry approval event + approval := telemetry.Enabled + if !settings.IsActive { + approval = telemetry.Disabled + } + + if err := telemetry.SendTelemetryApproval(*user, approval); err != nil { + fmt.Printf("Unable to send approval telemetry event: %s\n", err) + } + + // Write telemetry + if err := settings.Write(); err != nil { + fmt.Printf("Error writing telemetry settings to disk: %s\n", err) + } + + return +} diff --git a/cmd/check_telemetry_test.go b/cmd/create_telemetry_test.go similarity index 54% rename from cmd/check_telemetry_test.go rename to cmd/create_telemetry_test.go index c0e994552..4b02fd72b 100644 --- a/cmd/check_telemetry_test.go +++ b/cmd/create_telemetry_test.go @@ -1,9 +1,6 @@ package cmd import ( - "fmt" - "net/http" - "net/http/httptest" "path/filepath" "testing" @@ -15,28 +12,65 @@ import ( type testTelemetry struct { events []telemetry.Event + User telemetry.User } func (cli *testTelemetry) Close() error { return nil } func (cli *testTelemetry) Track(event telemetry.Event) error { - cli.events = append(cli.events, event) + newEvent := event + properties := map[string]interface{}{} + if cli.User.UniqueID != "" { + properties["UUID"] = cli.User.UniqueID + } + + if cli.User.UserID != "" { + properties["user_id"] = cli.User.UserID + } + + properties["is_self_hosted"] = cli.User.IsSelfHosted + + if cli.User.OS != "" { + properties["os"] = cli.User.OS + } + + if cli.User.Version != "" { + properties["cli_version"] = cli.User.Version + } + + if cli.User.TeamName != "" { + properties["team_name"] = cli.User.TeamName + } + + if len(properties) > 0 { + newEvent.Properties = properties + } + + cli.events = append(cli.events, newEvent) return nil } -func TestAskForTelemetryApproval(t *testing.T) { +type telemetryTestUI struct { + Approved bool +} + +func (ui telemetryTestUI) AskUserToApproveTelemetry(message string) bool { + return ui.Approved +} + +type telemetryTestAPIClient struct { + id string + err error +} + +func (me telemetryTestAPIClient) getMyUserId() (string, error) { + return me.id, me.err +} + +func TestLoadTelemetrySettings(t *testing.T) { // Mock HTTP userId := "id" uniqueId := "unique-id" - response := fmt.Sprintf(`{"id":"%s","login":"login","name":"name"}`, userId) - var handler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, r.Method, "GET") - assert.Equal(t, r.URL.String(), "/me") - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(response)) - } - server := httptest.NewServer(handler) - defer server.Close() // Mock create UUID oldUUIDCreate := createUUID @@ -47,10 +81,10 @@ func TestAskForTelemetryApproval(t *testing.T) { type args struct { closeStdin bool promptApproval bool - config settings.TelemetrySettings + settings settings.TelemetrySettings } type want struct { - config settings.TelemetrySettings + settings settings.TelemetrySettings fileNotCreated bool telemetryEvents []telemetry.Event } @@ -65,17 +99,22 @@ func TestAskForTelemetryApproval(t *testing.T) { name: "Prompt approval should be saved in settings", args: args{ promptApproval: true, - config: settings.TelemetrySettings{}, + settings: settings.TelemetrySettings{}, }, want: want{ - config: settings.TelemetrySettings{ + settings: settings.TelemetrySettings{ IsActive: true, HasAnsweredPrompt: true, UserID: userId, UniqueID: uniqueId, }, telemetryEvents: []telemetry.Event{ - {Object: "cli-telemetry", Action: "enabled"}, + {Object: "cli-telemetry", Action: "enabled", + Properties: map[string]interface{}{ + "UUID": uniqueId, + "user_id": userId, + "is_self_hosted": false, + }}, }, }, }, @@ -83,16 +122,17 @@ func TestAskForTelemetryApproval(t *testing.T) { name: "Prompt disapproval should be saved in settings", args: args{ promptApproval: false, - config: settings.TelemetrySettings{}, + settings: settings.TelemetrySettings{}, }, want: want{ - config: settings.TelemetrySettings{ + settings: settings.TelemetrySettings{ IsActive: false, HasAnsweredPrompt: true, - UniqueID: uniqueId, }, telemetryEvents: []telemetry.Event{ - {Object: "cli-telemetry", Action: "disabled"}, + {Object: "cli-telemetry", Action: "disabled", Properties: map[string]interface{}{ + "is_self_hosted": false, + }}, }, }, }, @@ -100,42 +140,37 @@ func TestAskForTelemetryApproval(t *testing.T) { name: "Does not recreate a unique ID if there is one", args: args{ promptApproval: true, - config: settings.TelemetrySettings{ + settings: settings.TelemetrySettings{ UniqueID: "other-id", }, }, want: want{ - config: settings.TelemetrySettings{ + settings: settings.TelemetrySettings{ IsActive: true, HasAnsweredPrompt: true, UserID: userId, UniqueID: "other-id", }, telemetryEvents: []telemetry.Event{ - {Object: "cli-telemetry", Action: "enabled"}, + {Object: "cli-telemetry", Action: "enabled", Properties: map[string]interface{}{ + "UUID": "other-id", + "user_id": userId, + "is_self_hosted": false, + }}, }, }, }, { name: "Does not change telemetry settings if user already answered prompt", args: args{ - config: settings.TelemetrySettings{ + settings: settings.TelemetrySettings{ HasAnsweredPrompt: true, }, }, want: want{ - fileNotCreated: true, - telemetryEvents: []telemetry.Event{}, - }, - }, - { - name: "Does not change telemetry settings if user disabled telemetry", - args: args{ - config: settings.TelemetrySettings{ - DisabledFromParams: true, + settings: settings.TelemetrySettings{ + HasAnsweredPrompt: true, }, - }, - want: want{ fileNotCreated: true, telemetryEvents: []telemetry.Event{}, }, @@ -146,7 +181,9 @@ func TestAskForTelemetryApproval(t *testing.T) { want: want{ fileNotCreated: true, telemetryEvents: []telemetry.Event{ - {Object: "cli-telemetry", Action: "disabled_default"}, + {Object: "cli-telemetry", Action: "disabled_default", Properties: map[string]interface{}{ + "is_self_hosted": false, + }}, }, }, }, @@ -160,27 +197,22 @@ func TestAskForTelemetryApproval(t *testing.T) { defer (func() { settings.FS.Fs = oldFS })() // Mock stdin - oldIsStdinOpen := isStdinOpen - isStdinOpen = !tt.args.closeStdin - defer (func() { isStdinOpen = oldIsStdinOpen })() + oldIsStdinOpen := isStdinATTY + isStdinATTY = !tt.args.closeStdin + defer (func() { isStdinATTY = oldIsStdinOpen })() // Mock telemetry telemetryClient := testTelemetry{events: make([]telemetry.Event, 0)} oldCreateActiveTelemetry := telemetry.CreateActiveTelemetry - telemetry.CreateActiveTelemetry = func(_ telemetry.User) telemetry.Client { + telemetry.CreateActiveTelemetry = func(user telemetry.User) telemetry.Client { + telemetryClient.User = user return &telemetryClient } defer (func() { telemetry.CreateActiveTelemetry = oldCreateActiveTelemetry })() - // Run askForTelemetryApproval - config := settings.Config{ - Token: "testtoken", - HTTPClient: http.DefaultClient, - Host: server.URL, - Telemetry: tt.args.config, - } - err := askForTelemetryApproval(&config, telemetryTestUI{tt.args.promptApproval}) - assert.NilError(t, err) + // Run tested function + loadTelemetrySettings(&tt.args.settings, &telemetry.User{}, telemetryTestAPIClient{userId, nil}, telemetryTestUI{tt.args.promptApproval}) + assert.DeepEqual(t, &tt.args.settings, &tt.want.settings) // Verify good telemetry events were sent assert.DeepEqual(t, telemetryClient.events, tt.want.telemetryEvents) @@ -194,10 +226,10 @@ func TestAskForTelemetryApproval(t *testing.T) { } // Verify settings file content - result := settings.TelemetrySettings{} - err = result.Load() + loaded := settings.TelemetrySettings{} + err = loaded.Load() assert.NilError(t, err) - assert.Equal(t, result, tt.want.config) + assert.DeepEqual(t, &loaded, &tt.want.settings) }) } } diff --git a/cmd/root.go b/cmd/root.go index 6288231f2..12ce075e4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -126,11 +126,6 @@ func MakeCommands() *cobra.Command { PersistentPreRunE: func(_ *cobra.Command, _ []string) error { return rootCmdPreRun(rootOptions) }, - PersistentPostRun: func(cmd *cobra.Command, args []string) { - if rootOptions.Telemetry.Client != nil { - rootOptions.Telemetry.Client.Close() - } - }, } // For supporting "Args" in command usage help @@ -189,7 +184,7 @@ func MakeCommands() *cobra.Command { flags.StringVar(&rootOptions.Endpoint, "endpoint", rootOptions.Endpoint, "URI to your CircleCI GraphQL API endpoint") 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.Telemetry.DisabledFromParams, "disable-telemetry", false, "Do not show telemetry for the actual command") + flags.BoolVar(&rootOptions.IsTelemetryDisabled, "disable-telemetry", false, "Do not show telemetry for the actual command") hidden := []string{"github-api", "debug", "endpoint"} diff --git a/settings/settings.go b/settings/settings.go index 89f27df36..efde6c893 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -17,8 +17,6 @@ import ( yaml "gopkg.in/yaml.v3" "github.com/CircleCI-Public/circleci-cli/data" - "github.com/CircleCI-Public/circleci-cli/telemetry" - "github.com/CircleCI-Public/circleci-cli/version" "github.com/spf13/afero" ) @@ -30,22 +28,22 @@ 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:"-"` - OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"` - Telemetry TelemetrySettings `yaml:"-"` + 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"` } type OrbPublishingInfo struct { @@ -62,13 +60,10 @@ type UpdateCheck struct { // TelemetrySettings is used to represent telemetry related settings type TelemetrySettings struct { - IsActive bool `yaml:"is_active"` - HasAnsweredPrompt bool `yaml:"has_answered_prompt"` - DisabledFromParams bool `yaml:"-"` - UniqueID string `yaml:"unique_id"` - UserID string `yaml:"user_id"` - - Client telemetry.Client `yaml:"-"` + IsActive bool `yaml:"is_active"` + HasAnsweredPrompt bool `yaml:"has_answered_prompt"` + UniqueID string `yaml:"unique_id"` + UserID string `yaml:"user_id"` } // Load will read the update check settings from the user's disk and then deserialize it into the current instance. @@ -130,35 +125,6 @@ func (tel *TelemetrySettings) Write() error { return err } -// Track takes a telemetry event, enrich with various data and sends it -// This is the method you must use to send telemetry events -// This will fail if 'checkTelemetry' has not called before -func (cfg *Config) Track(event telemetry.Event) error { - if cfg.Telemetry.Client == nil { - return errors.New("No telemetry client found") - } - - if cfg.Telemetry.UniqueID != "" { - event.Properties["UUID"] = cfg.Telemetry.UniqueID - } - - if cfg.Telemetry.UserID != "" { - event.Properties["user_id"] = cfg.Telemetry.UserID - } - - if cfg.Host != "" { - event.Properties["host"] = cfg.Host - } else { - event.Properties["host"] = "https://circleci.com" - } - - event.Properties["os"] = runtime.GOOS - event.Properties["cli_version"] = version.Version - event.Properties["team_name"] = "devex" - - return cfg.Telemetry.Client.Track(event) -} - // Load will read the config from the user's disk and then evaluate possible configuration from the environment. func (cfg *Config) Load() error { if err := cfg.LoadFromDisk(); err != nil { diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 7bda42412..092c4253f 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -1,6 +1,7 @@ package telemetry import ( + "fmt" "io" "github.com/segmentio/analytics-go" @@ -32,12 +33,15 @@ type Event struct { } type User struct { - UniqueID string - - UserID string + UniqueID string + UserID string + IsSelfHosted bool + OS string + Version string + TeamName string } -// Create a telemetry client to be used to send telemetry event +// Create a telemetry client to be used to send telemetry events func CreateClient(user User, enabled bool) Client { if !enabled { return nullClient{} @@ -69,8 +73,8 @@ func (cli nullClient) Track(_ Event) error { return nil } // Used when telemetry is enabled type segmentClient struct { - cli analytics.Client - userId string + cli analytics.Client + user User } const ( @@ -85,17 +89,15 @@ func newSegmentClient(user User) Client { userID = "none" } - traits := analytics.NewTraits().Set("UUID", user.UniqueID) - if user.UserID != "" { - traits = traits.Set("userId", user.UserID) - } - cli.Enqueue( + err := cli.Enqueue( analytics.Identify{ UserId: userID, + Traits: analytics.NewTraits().Set("os", user.OS), }, ) + fmt.Printf("Error while identifying with telemetry: %s\n", err) - return &segmentClient{cli, userID} + return &segmentClient{cli, user} } func (segment *segmentClient) Track(event Event) error { @@ -103,8 +105,31 @@ func (segment *segmentClient) Track(event Event) error { event.Properties = make(map[string]interface{}) } event.Properties["action"] = event.Action + + if segment.user.UniqueID != "" { + event.Properties["UUID"] = segment.user.UniqueID + } + + if segment.user.UserID != "" { + event.Properties["user_id"] = segment.user.UserID + } + + event.Properties["is_self_hosted"] = segment.user.IsSelfHosted + + if segment.user.OS != "" { + event.Properties["os"] = segment.user.OS + } + + if segment.user.Version != "" { + event.Properties["cli_version"] = segment.user.Version + } + + if segment.user.TeamName != "" { + event.Properties["team_name"] = segment.user.TeamName + } + return segment.cli.Enqueue(analytics.Track{ - UserId: segment.userId, + UserId: segment.user.UniqueID, Event: event.Object, Properties: event.Properties, }) From 91caef47781dff570ff344f819717efd7842406e Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Wed, 5 Jul 2023 16:27:40 +0200 Subject: [PATCH 10/36] feat: Added `circleci telemetry enable` and `circleci telemetry disable` commands --- cmd/root.go | 1 + cmd/root_test.go | 2 +- cmd/telemetry.go | 73 +++++++++++++++++++++ cmd/telemetry_test.go | 143 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 cmd/telemetry.go create mode 100644 cmd/telemetry_test.go diff --git a/cmd/root.go b/cmd/root.go index 12ce075e4..8139a2bd2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -175,6 +175,7 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newAdminCommand(rootOptions)) rootCmd.AddCommand(newCompletionCommand()) rootCmd.AddCommand(newEnvCmd()) + rootCmd.AddCommand(newTelemetryCommand(rootOptions)) flags := rootCmd.PersistentFlags() diff --git a/cmd/root_test.go b/cmd/root_test.go index c23982b18..515a9294e 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -16,7 +16,7 @@ var _ = Describe("Root", func() { Describe("subcommands", func() { It("can create commands", func() { commands := cmd.MakeCommands() - Expect(len(commands.Commands())).To(Equal(24)) + Expect(len(commands.Commands())).To(Equal(25)) }) }) diff --git a/cmd/telemetry.go b/cmd/telemetry.go new file mode 100644 index 000000000..df2be673c --- /dev/null +++ b/cmd/telemetry.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "os" + + "github.com/CircleCI-Public/circleci-cli/api/rest" + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func newTelemetryCommand(config *settings.Config) *cobra.Command { + apiClient := telemetryCircleCIAPI{ + cli: rest.NewFromConfig(config.Host, config), + } + + telemetryEnable := &cobra.Command{ + Use: "enable", + Short: "Allow telemetry events to be sent to CircleCI servers", + RunE: func(_ *cobra.Command, _ []string) error { + return setIsTelemetryActive(apiClient, true) + }, + Args: cobra.ExactArgs(0), + } + + telemetryDisable := &cobra.Command{ + Use: "disable", + Short: "Make sure no telemetry events is sent to CircleCI servers", + RunE: func(_ *cobra.Command, _ []string) error { + return setIsTelemetryActive(apiClient, false) + }, + Args: cobra.ExactArgs(0), + } + + telemetryCommand := &cobra.Command{ + Use: "telemetry", + Short: "Configure telemetry preferences", + Long: `Configure telemetry preferences. + +Note: If you have not configured your telemetry preferences and call the CLI with a closed stdin, telemetry will be disabled`, + } + + telemetryCommand.AddCommand(telemetryEnable) + telemetryCommand.AddCommand(telemetryDisable) + + return telemetryCommand +} + +func setIsTelemetryActive(apiClient telemetryAPIClient, isActive bool) error { + settings := settings.TelemetrySettings{} + if err := settings.Load(); err != nil && !os.IsNotExist(err) { + return errors.Wrap(err, "Loading telemetry configuration") + } + + settings.HasAnsweredPrompt = true + settings.IsActive = isActive + + if settings.UniqueID == "" { + settings.UniqueID = createUUID() + } + + if settings.UserID == "" { + if myID, err := apiClient.getMyUserId(); err == nil { + settings.UserID = myID + } + } + + if err := settings.Write(); err != nil { + return errors.Wrap(err, "Writing telemetry configuration") + } + + return nil +} diff --git a/cmd/telemetry_test.go b/cmd/telemetry_test.go new file mode 100644 index 000000000..35eeb848b --- /dev/null +++ b/cmd/telemetry_test.go @@ -0,0 +1,143 @@ +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/spf13/afero" + "gotest.tools/v3/assert" +) + +func TestSetIsTelemetryActive(t *testing.T) { + type args struct { + apiClient telemetryAPIClient + isActive bool + settings *settings.TelemetrySettings + } + type want struct { + settings *settings.TelemetrySettings + } + + type testCase struct { + name string + args args + want want + } + + userId := "user-id" + uniqueId := "unique-id" + + testCases := []testCase{ + { + name: "Enabling telemetry with settings should just update the is active field", + args: args{ + apiClient: telemetryTestAPIClient{}, + isActive: true, + settings: &settings.TelemetrySettings{ + IsActive: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsActive: true, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + { + name: "Enabling telemetry without settings should fill the settings fields", + args: args{ + apiClient: telemetryTestAPIClient{id: userId, err: nil}, + isActive: true, + settings: nil, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsActive: true, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + { + name: "Disabling telemetry with settings should just update the is active field", + args: args{ + apiClient: telemetryTestAPIClient{}, + isActive: false, + settings: &settings.TelemetrySettings{ + IsActive: true, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsActive: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + { + name: "Enabling telemetry without settings should fill the settings fields", + args: args{ + apiClient: telemetryTestAPIClient{id: userId, err: nil}, + isActive: false, + settings: nil, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsActive: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + } + + // Mock create UUID + oldUUIDCreate := createUUID + createUUID = func() string { return uniqueId } + defer (func() { createUUID = oldUUIDCreate })() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + // Mock FS + oldFS := settings.FS.Fs + settings.FS.Fs = afero.NewMemMapFs() + defer (func() { settings.FS.Fs = oldFS })() + + if tt.args.settings != nil { + err := tt.args.settings.Write() + assert.NilError(t, err) + } + + err := setIsTelemetryActive(tt.args.apiClient, tt.args.isActive) + assert.NilError(t, err) + + exist, err := settings.FS.Exists(filepath.Join(settings.SettingsPath(), "telemetry.yml")) + assert.NilError(t, err) + if tt.want.settings == nil { + assert.Equal(t, exist, false) + } else { + assert.Equal(t, exist, true) + + loadedSettings := &settings.TelemetrySettings{} + err := loadedSettings.Load() + assert.NilError(t, err) + + assert.DeepEqual(t, tt.want.settings, loadedSettings) + } + }) + } +} From 71593f30defc396d49a89f74781c3748daec362a Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Thu, 6 Jul 2023 11:56:15 +0200 Subject: [PATCH 11/36] chore: Added telemetry events --- clitest/clitest.go | 10 +++++++++ cmd/create_telemetry.go | 19 +++++++++++++---- cmd/root.go | 3 ++- cmd/setup.go | 7 +++++++ cmd/setup_test.go | 36 +++++++++++++++++++++++++++++++++ cmd/version.go | 9 +++++++++ settings/settings.go | 35 +++++++++++++++++--------------- telemetry/events.go | 24 ++++++++++++++++++++++ telemetry/telemetry.go | 45 ++++++++++++++++++++++++++++++++++++++--- 9 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 telemetry/events.go diff --git a/clitest/clitest.go b/clitest/clitest.go index 62e83180b..ae7bd8033 100644 --- a/clitest/clitest.go +++ b/clitest/clitest.go @@ -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" ) @@ -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() diff --git a/cmd/create_telemetry.go b/cmd/create_telemetry.go index 95e54105d..f5eaf3efc 100644 --- a/cmd/create_telemetry.go +++ b/cmd/create_telemetry.go @@ -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{} @@ -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 } diff --git a/cmd/root.go b/cmd/root.go index 8139a2bd2..c85759851 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 { diff --git a/cmd/setup.go b/cmd/setup.go index 6a2d068b5..396413655 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -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" ) @@ -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 } diff --git a/cmd/setup_test.go b/cmd/setup_test.go index 591a6197d..99e4c6620 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -5,6 +5,7 @@ import ( "io" "os" "os/exec" + "path/filepath" "regexp" "runtime" @@ -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) diff --git a/cmd/version.go b/cmd/version.go index 756e5e6ce..c8039a44d 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -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" ) @@ -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) { @@ -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() + }, } } diff --git a/settings/settings.go b/settings/settings.go index efde6c893..b31797fe2 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -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 { diff --git a/telemetry/events.go b/telemetry/events.go new file mode 100644 index 000000000..41ba03c8f --- /dev/null +++ b/telemetry/events.go @@ -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{}{}, + } +} diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 092c4253f..bb36fb2e9 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -1,8 +1,10 @@ package telemetry import ( + "encoding/json" "fmt" "io" + "os" "github.com/segmentio/analytics-go" ) @@ -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 { @@ -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{} } @@ -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() +} From 9bacba5565820e31c76b0aa31e6c0aa0b8aca14a Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Mon, 10 Jul 2023 18:56:55 +0200 Subject: [PATCH 12/36] chore: Added events for update, version and diagnostic Also changed the way we check for telemetry events in E2E tests --- clitest/telemetry.go | 19 +++++++++++++ cmd/create_telemetry.go | 27 ++++++++++++++++++ cmd/diagnostic.go | 5 ++++ cmd/setup_test.go | 10 +++---- cmd/update.go | 15 ++++------ cmd/update_test.go | 62 +++++++++++++++++++++++++++++++++++++++++ cmd/version.go | 2 +- telemetry/events.go | 39 ++++++++++++++++++++++---- telemetry/telemetry.go | 35 ++++++++++++----------- 9 files changed, 177 insertions(+), 37 deletions(-) create mode 100644 clitest/telemetry.go diff --git a/clitest/telemetry.go b/clitest/telemetry.go new file mode 100644 index 000000000..cb7dee2ce --- /dev/null +++ b/clitest/telemetry.go @@ -0,0 +1,19 @@ +package clitest + +import ( + "encoding/json" + "os" + + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/onsi/gomega" +) + +func CompareTelemetryEvent(filePath string, expected []telemetry.Event) { + content, err := os.ReadFile(filePath) + 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)) +} diff --git a/cmd/create_telemetry.go b/cmd/create_telemetry.go index f5eaf3efc..340a82b78 100644 --- a/cmd/create_telemetry.go +++ b/cmd/create_telemetry.go @@ -12,6 +12,8 @@ import ( "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/version" "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/term" ) @@ -164,3 +166,28 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry fmt.Printf("Error writing telemetry settings to disk: %s\n", err) } } + +// Utility function used when creating telemetry events. +// It takes a cobra Command and creates a telemetry.CommandInfo of it +// If getParent is true, puts both the command's args in `LocalArgs` and the parent's args +// Else only put the command's args +// Note: child flags overwrite parent flags with same name +func getCommandInformation(cmd *cobra.Command, getParent bool) telemetry.CommandInfo { + localArgs := map[string]string{} + + parent := cmd.Parent() + if getParent && parent != nil { + parent.LocalFlags().VisitAll(func(flag *pflag.Flag) { + localArgs[flag.Name] = flag.Value.String() + }) + } + + cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) { + localArgs[flag.Name] = flag.Value.String() + }) + + return telemetry.CommandInfo{ + Name: cmd.Name(), + LocalArgs: localArgs, + } +} diff --git a/cmd/diagnostic.go b/cmd/diagnostic.go index 071ceb257..9624c35c9 100644 --- a/cmd/diagnostic.go +++ b/cmd/diagnostic.go @@ -7,6 +7,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" ) @@ -25,6 +26,10 @@ 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) }, diff --git a/cmd/setup_test.go b/cmd/setup_test.go index 99e4c6620..939b7b31a 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -10,6 +10,7 @@ import ( "runtime" "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" @@ -59,15 +60,14 @@ var _ = Describe("Setup with prompts", func() { } }) - It("should send telemetry event when", func() { + It("should send telemetry event", 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}} -`)) + clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + telemetry.CreateSetupEvent(false), + }) }) }) diff --git a/cmd/update.go b/cmd/update.go index c43bf33d4..b53278703 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -5,6 +5,7 @@ import ( "time" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/update" "github.com/CircleCI-Public/circleci-cli/version" @@ -28,8 +29,11 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { update := &cobra.Command{ Use: "update", Short: "Update the tool to the latest version", - PersistentPreRun: func(_ *cobra.Command, _ []string) { + PersistentPreRun: func(cmd *cobra.Command, _ []string) { opts.cfg.SkipUpdateCheck = true + telemetryClient := createTelemetry(config) + defer telemetryClient.Close() + telemetryClient.Track(telemetry.CreateUpdateEvent(getCommandInformation(cmd, cmd.Name() != "update"))) }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args @@ -43,9 +47,6 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { Use: "check", Hidden: true, Short: "Check if there are any updates available", - PersistentPreRun: func(_ *cobra.Command, _ []string) { - opts.cfg.SkipUpdateCheck = true - }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args opts.dryRun = true @@ -59,9 +60,6 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { Use: "install", Hidden: true, Short: "Update the tool to the latest version", - PersistentPreRun: func(_ *cobra.Command, _ []string) { - opts.cfg.SkipUpdateCheck = true - }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args }, @@ -74,9 +72,6 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { Use: "build-agent", Hidden: true, Short: "This command has no effect, and is kept for backwards compatibility", - PersistentPreRun: func(_ *cobra.Command, _ []string) { - opts.cfg.SkipUpdateCheck = true - }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args }, diff --git a/cmd/update_test.go b/cmd/update_test.go index 431987119..238b32ef3 100644 --- a/cmd/update_test.go +++ b/cmd/update_test.go @@ -2,10 +2,12 @@ package cmd_test import ( "net/http" + "os" "os/exec" "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/gbytes" @@ -70,6 +72,66 @@ var _ = Describe("Update", func() { tempSettings.Close() }) + Describe("telemetry", func() { + var ( + telemetryDestFilePath string + ) + + BeforeEach(func() { + telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") + }) + + AfterEach(func() { + tempSettings.Close() + if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { + os.Remove(telemetryDestFilePath) + } + }) + + It("should send telemetry event when calling parent command", func() { + command = exec.Command(pathCLI, + "update", + "--github-api", tempSettings.TestServer.URL(), + "--mock-telemetry", telemetryDestFilePath, + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + telemetry.CreateUpdateEvent(telemetry.CommandInfo{ + Name: "update", + LocalArgs: map[string]string{ + "check": "false", + "help": "false", + }, + }), + }) + }) + + It("should send telemetry event when calling child command", func() { + command = exec.Command(pathCLI, + "update", + "check", + "--github-api", tempSettings.TestServer.URL(), + "--mock-telemetry", telemetryDestFilePath, + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + telemetry.CreateUpdateEvent(telemetry.CommandInfo{ + Name: "check", + LocalArgs: map[string]string{ + "check": "false", + "help": "false", + }, + }), + }) + }) + }) + Describe("update --check", func() { BeforeEach(func() { command = exec.Command(pathCLI, diff --git a/cmd/version.go b/cmd/version.go index c8039a44d..473a43a3f 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -34,7 +34,7 @@ func newVersionCommand(config *settings.Config) *cobra.Command { 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 { + if err := telemetryClient.Track(telemetry.CreateVersionEvent(version.Version)); err != nil { return err } return telemetryClient.Close() diff --git a/telemetry/events.go b/telemetry/events.go index 41ba03c8f..b01635fa3 100644 --- a/telemetry/events.go +++ b/telemetry/events.go @@ -1,24 +1,53 @@ package telemetry +import "fmt" + // 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 +type CommandInfo struct { + Name string + LocalArgs map[string]string +} + +func localArgsToProperties(cmdInfo CommandInfo) map[string]interface{} { + properties := map[string]interface{}{} + for key, value := range cmdInfo.LocalArgs { + properties[fmt.Sprintf("cmd.flag.%s", key)] = value + } + return properties +} + func CreateSetupEvent(isServerCustomer bool) Event { return Event{ Object: "cli-setup", - Action: "called", Properties: map[string]interface{}{ "is_server_customer": isServerCustomer, }, } } -func CreateVersionEvent() Event { +func CreateVersionEvent(version string) Event { + return Event{ + Object: "cli-version", + Properties: map[string]interface{}{ + "version": version, + }, + } +} + +func CreateUpdateEvent(cmdInfo CommandInfo) Event { + return Event{ + Object: "cli-update", + Action: cmdInfo.Name, + Properties: localArgsToProperties(cmdInfo), + } +} + +func CreateDiagnosticEvent() Event { return Event{ - Object: "cli-version", - Action: "called", - Properties: map[string]interface{}{}, + Object: "cli-diagnostic", } } diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index bb36fb2e9..db3e24200 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -48,7 +48,6 @@ 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{} } @@ -110,7 +109,9 @@ func (segment *segmentClient) Track(event Event) error { if event.Properties == nil { event.Properties = make(map[string]interface{}) } - event.Properties["action"] = event.Action + if event.Action != "" { + event.Properties["action"] = event.Action + } if segment.user.UniqueID != "" { event.Properties["UUID"] = segment.user.UniqueID @@ -149,31 +150,33 @@ func (segment *segmentClient) Close() error { // Used for E2E tests type fileTelemetry struct { - file *os.File + filePath string + events []Event } 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} + return &fileTelemetry{filePath, make([]Event, 0)} } func (cli *fileTelemetry) Track(event Event) error { - content, err := json.Marshal(&event) + cli.events = append(cli.events, event) + return nil +} + +func (cli *fileTelemetry) Close() error { + file, err := os.OpenFile(cli.filePath, os.O_CREATE|os.O_WRONLY, 0666) if err != nil { return err } - content = append(content, '\n') - _, err = cli.file.Write(content) + content, err := json.Marshal(&cli.events) + if err != nil { + return err + } - return err -} + if _, err = file.Write(content); err != nil { + return err + } -func (cli *fileTelemetry) Close() error { - file := cli.file - cli.file = nil return file.Close() } From c0c284e1b33e9ab7ebcf8f1b8f55d206e257fce8 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 11 Jul 2023 10:28:59 +0200 Subject: [PATCH 13/36] 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), } } From f0c6f29f07ce7446744f4d8b12e68bfe071c02bd Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 11 Jul 2023 10:44:48 +0200 Subject: [PATCH 14/36] chore: Moved create telemetry in its own module --- cmd/completion.go | 5 +-- cmd/config.go | 12 +++---- .../create_telemetry.go | 34 +++++++++++-------- .../create_telemetry_test.go | 10 +++--- cmd/diagnostic.go | 3 +- cmd/follow.go | 3 +- cmd/open.go | 3 +- cmd/setup.go | 3 +- cmd/telemetry.go | 12 +++---- cmd/telemetry_test.go | 18 +++++++--- cmd/update.go | 5 +-- cmd/version.go | 3 +- 12 files changed, 65 insertions(+), 46 deletions(-) rename cmd/{ => create_telemetry}/create_telemetry.go (84%) rename cmd/{ => create_telemetry}/create_telemetry_test.go (96%) diff --git a/cmd/completion.go b/cmd/completion.go index 8346852e0..43b6aa948 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -3,6 +3,7 @@ package cmd import ( "os" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" @@ -13,9 +14,9 @@ func newCompletionCommand(config *settings.Config) *cobra.Command { Use: "completion", Short: "Generate shell completion scripts", PersistentPreRun: func(cmd *cobra.Command, _ []string) { - telemetryClient := createTelemetry(config) + telemetryClient := create_telemetry.CreateTelemetry(config) defer telemetryClient.Close() - telemetryClient.Track(telemetry.CreateCompletionCommand(getCommandInformation(cmd, false))) + telemetryClient.Track(telemetry.CreateCompletionCommand(create_telemetry.GetCommandInformation(cmd, false))) }, Run: func(cmd *cobra.Command, _ []string) { err := cmd.Help() diff --git a/cmd/config.go b/cmd/config.go index da669e54b..ea680a690 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,10 +8,7 @@ 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/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/config" "github.com/CircleCI-Public/circleci-cli/filetree" "github.com/CircleCI-Public/circleci-cli/proxy" @@ -46,7 +43,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { Use: "config", Short: "Operate on build config files", PersistentPreRun: func(_ *cobra.Command, _ []string) { - telemetryClient = createTelemetry(globalConfig) + telemetryClient = create_telemetry.CreateTelemetry(globalConfig) }, PersistentPostRun: func(cmd *cobra.Command, args []string) { closeTelemetryClient() @@ -59,7 +56,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { defer closeTelemetryClient() err := packConfig(args) - telemetryClient.Track(telemetry.CreateConfigEvent(getCommandInformation(cmd, true))) + telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true))) return err }, Args: cobra.ExactArgs(1), @@ -92,7 +89,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { IgnoreDeprecatedImages: ignoreDeprecatedImages, VerboseOutput: verboseOutput, }) - telemetryClient.Track(telemetry.CreateConfigEvent(getCommandInformation(cmd, true))) + telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true))) return err }, @@ -135,6 +132,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { PipelineParamsFilePath: pipelineParamsFilePath, VerboseOutput: verboseOutput, }) + telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true))) if err != nil { return err } diff --git a/cmd/create_telemetry.go b/cmd/create_telemetry/create_telemetry.go similarity index 84% rename from cmd/create_telemetry.go rename to cmd/create_telemetry/create_telemetry.go index 340a82b78..99b98d4a5 100644 --- a/cmd/create_telemetry.go +++ b/cmd/create_telemetry/create_telemetry.go @@ -1,4 +1,4 @@ -package cmd +package create_telemetry import ( "fmt" @@ -18,7 +18,7 @@ import ( ) var ( - createUUID = func() string { return uuid.New().String() } + CreateUUID = func() string { return uuid.New().String() } isStdinATTY = term.IsTerminal(int(os.Stdin.Fd())) ) @@ -32,15 +32,21 @@ func (telemetryInteractiveUI) AskUserToApproveTelemetry(message string) bool { return prompt.AskUserToConfirmWithDefault(message, true) } -type telemetryAPIClient interface { - getMyUserId() (string, error) +type TelemetryAPIClient interface { + GetMyUserId() (string, error) +} + +func CreateAPIClient(config *settings.Config) TelemetryAPIClient { + return telemetryCircleCIAPI{ + cli: rest.NewFromConfig(config.Host, config), + } } type telemetryCircleCIAPI struct { cli *rest.Client } -func (client telemetryCircleCIAPI) getMyUserId() (string, error) { +func (client telemetryCircleCIAPI) GetMyUserId() (string, error) { me, err := api.GetMe(client.cli) if err != nil { return "", err @@ -50,12 +56,12 @@ func (client telemetryCircleCIAPI) getMyUserId() (string, error) { type nullTelemetryAPIClient struct{} -func (client nullTelemetryAPIClient) getMyUserId() (string, error) { +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 { +func CreateTelemetry(config *settings.Config) telemetry.Client { if config.MockTelemetry != "" { return telemetry.CreateFileTelemetry(config.MockTelemetry) } @@ -64,7 +70,7 @@ func createTelemetry(config *settings.Config) telemetry.Client { return telemetry.CreateClient(telemetry.User{}, false) } - var apiClient telemetryAPIClient = nullTelemetryAPIClient{} + var apiClient TelemetryAPIClient = nullTelemetryAPIClient{} if config.HTTPClient != nil { apiClient = telemetryCircleCIAPI{ cli: rest.NewFromConfig(config.Host, config), @@ -74,7 +80,7 @@ func createTelemetry(config *settings.Config) telemetry.Client { telemetrySettings := settings.TelemetrySettings{} user := telemetry.User{ - IsSelfHosted: config.Host == defaultHost, + IsSelfHosted: config.Host == "https://circleci.com", OS: runtime.GOOS, Version: version.Version, TeamName: "devex", @@ -86,7 +92,7 @@ func createTelemetry(config *settings.Config) telemetry.Client { return client } -func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry.User, apiClient telemetryAPIClient, ui telemetryUI) { +func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry.User, apiClient TelemetryAPIClient, ui telemetryUI) { err := settings.Load() if err != nil && !os.IsNotExist(err) { fmt.Printf("Error loading telemetry configuration: %s\n", err) @@ -99,7 +105,7 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry if settings.HasAnsweredPrompt { // If we have no user id, we try requesting the user id again if settings.UserID == "" && settings.IsActive { - myID, err := apiClient.getMyUserId() + myID, err := apiClient.GetMyUserId() if err == nil { settings.UserID = myID user.UserID = myID @@ -136,12 +142,12 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry // Make sure we have user info and set them if settings.IsActive { if settings.UniqueID == "" { - settings.UniqueID = createUUID() + settings.UniqueID = CreateUUID() } user.UniqueID = settings.UniqueID if settings.UserID == "" { - myID, err := apiClient.getMyUserId() + myID, err := apiClient.GetMyUserId() if err == nil { settings.UserID = myID } @@ -172,7 +178,7 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry // If getParent is true, puts both the command's args in `LocalArgs` and the parent's args // Else only put the command's args // Note: child flags overwrite parent flags with same name -func getCommandInformation(cmd *cobra.Command, getParent bool) telemetry.CommandInfo { +func GetCommandInformation(cmd *cobra.Command, getParent bool) telemetry.CommandInfo { localArgs := map[string]string{} parent := cmd.Parent() diff --git a/cmd/create_telemetry_test.go b/cmd/create_telemetry/create_telemetry_test.go similarity index 96% rename from cmd/create_telemetry_test.go rename to cmd/create_telemetry/create_telemetry_test.go index 4b02fd72b..d573d0042 100644 --- a/cmd/create_telemetry_test.go +++ b/cmd/create_telemetry/create_telemetry_test.go @@ -1,4 +1,4 @@ -package cmd +package create_telemetry import ( "path/filepath" @@ -63,7 +63,7 @@ type telemetryTestAPIClient struct { err error } -func (me telemetryTestAPIClient) getMyUserId() (string, error) { +func (me telemetryTestAPIClient) GetMyUserId() (string, error) { return me.id, me.err } @@ -73,9 +73,9 @@ func TestLoadTelemetrySettings(t *testing.T) { uniqueId := "unique-id" // Mock create UUID - oldUUIDCreate := createUUID - createUUID = func() string { return uniqueId } - defer (func() { createUUID = oldUUIDCreate })() + oldUUIDCreate := CreateUUID + CreateUUID = func() string { return uniqueId } + defer (func() { CreateUUID = oldUUIDCreate })() // Create test cases type args struct { diff --git a/cmd/diagnostic.go b/cmd/diagnostic.go index 24dce3251..898c97222 100644 --- a/cmd/diagnostic.go +++ b/cmd/diagnostic.go @@ -6,6 +6,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" @@ -30,7 +31,7 @@ func newDiagnosticCommand(config *settings.Config) *cobra.Command { opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) }, RunE: func(_ *cobra.Command, _ []string) error { - telemetryClient := createTelemetry(config) + telemetryClient := create_telemetry.CreateTelemetry(config) defer telemetryClient.Close() err := diagnostic(opts) diff --git a/cmd/follow.go b/cmd/follow.go index e3afa40a5..89005badd 100644 --- a/cmd/follow.go +++ b/cmd/follow.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/git" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -55,7 +56,7 @@ 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 { - telemetryClient := createTelemetry(config) + telemetryClient := create_telemetry.CreateTelemetry(config) defer telemetryClient.Close() err := followProject(opts) diff --git a/cmd/open.go b/cmd/open.go index d53c08440..e1cbc4adf 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -5,6 +5,7 @@ import ( "net/url" "strings" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/git" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -46,7 +47,7 @@ func newOpenCommand(config *settings.Config) *cobra.Command { Use: "open", Short: "Open the current project in the browser.", RunE: func(_ *cobra.Command, _ []string) error { - telemetryClient := createTelemetry(config) + telemetryClient := create_telemetry.CreateTelemetry(config) defer telemetryClient.Close() err := openProjectInBrowser() diff --git a/cmd/setup.go b/cmd/setup.go index 396413655..2c8e174a5 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -5,6 +5,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/prompt" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -190,7 +191,7 @@ func setup(opts setupOptions) error { setupDiagnosticCheck(opts) } - telemetryClient := createTelemetry(opts.cfg) + telemetryClient := create_telemetry.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) diff --git a/cmd/telemetry.go b/cmd/telemetry.go index df2be673c..89c52192d 100644 --- a/cmd/telemetry.go +++ b/cmd/telemetry.go @@ -3,16 +3,14 @@ package cmd import ( "os" - "github.com/CircleCI-Public/circleci-cli/api/rest" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/pkg/errors" "github.com/spf13/cobra" ) func newTelemetryCommand(config *settings.Config) *cobra.Command { - apiClient := telemetryCircleCIAPI{ - cli: rest.NewFromConfig(config.Host, config), - } + apiClient := create_telemetry.CreateAPIClient(config) telemetryEnable := &cobra.Command{ Use: "enable", @@ -46,7 +44,7 @@ Note: If you have not configured your telemetry preferences and call the CLI wit return telemetryCommand } -func setIsTelemetryActive(apiClient telemetryAPIClient, isActive bool) error { +func setIsTelemetryActive(apiClient create_telemetry.TelemetryAPIClient, isActive bool) error { settings := settings.TelemetrySettings{} if err := settings.Load(); err != nil && !os.IsNotExist(err) { return errors.Wrap(err, "Loading telemetry configuration") @@ -56,11 +54,11 @@ func setIsTelemetryActive(apiClient telemetryAPIClient, isActive bool) error { settings.IsActive = isActive if settings.UniqueID == "" { - settings.UniqueID = createUUID() + settings.UniqueID = create_telemetry.CreateUUID() } if settings.UserID == "" { - if myID, err := apiClient.getMyUserId(); err == nil { + if myID, err := apiClient.GetMyUserId(); err == nil { settings.UserID = myID } } diff --git a/cmd/telemetry_test.go b/cmd/telemetry_test.go index 35eeb848b..764927de2 100644 --- a/cmd/telemetry_test.go +++ b/cmd/telemetry_test.go @@ -4,14 +4,24 @@ import ( "path/filepath" "testing" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/spf13/afero" "gotest.tools/v3/assert" ) +type telemetryTestAPIClient struct { + id string + err error +} + +func (me telemetryTestAPIClient) GetMyUserId() (string, error) { + return me.id, me.err +} + func TestSetIsTelemetryActive(t *testing.T) { type args struct { - apiClient telemetryAPIClient + apiClient create_telemetry.TelemetryAPIClient isActive bool settings *settings.TelemetrySettings } @@ -106,9 +116,9 @@ func TestSetIsTelemetryActive(t *testing.T) { } // Mock create UUID - oldUUIDCreate := createUUID - createUUID = func() string { return uniqueId } - defer (func() { createUUID = oldUUIDCreate })() + oldUUIDCreate := create_telemetry.CreateUUID + create_telemetry.CreateUUID = func() string { return uniqueId } + defer (func() { create_telemetry.CreateUUID = oldUUIDCreate })() for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/update.go b/cmd/update.go index b53278703..d11941082 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/update" @@ -31,9 +32,9 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { Short: "Update the tool to the latest version", PersistentPreRun: func(cmd *cobra.Command, _ []string) { opts.cfg.SkipUpdateCheck = true - telemetryClient := createTelemetry(config) + telemetryClient := create_telemetry.CreateTelemetry(config) defer telemetryClient.Close() - telemetryClient.Track(telemetry.CreateUpdateEvent(getCommandInformation(cmd, cmd.Name() != "update"))) + telemetryClient.Track(telemetry.CreateUpdateEvent(create_telemetry.GetCommandInformation(cmd, cmd.Name() != "update"))) }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args diff --git a/cmd/version.go b/cmd/version.go index 473a43a3f..7c6f5d058 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/version" @@ -24,7 +25,7 @@ func newVersionCommand(config *settings.Config) *cobra.Command { Use: "version", Short: "Display version information", PersistentPreRun: func(_ *cobra.Command, _ []string) { - telemetryClient = createTelemetry(config) + telemetryClient = create_telemetry.CreateTelemetry(config) opts.cfg.SkipUpdateCheck = true }, PreRun: func(cmd *cobra.Command, args []string) { From 2761dc6202935fb374fd45f73a98c012a4b5bac8 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 11 Jul 2023 16:07:34 +0200 Subject: [PATCH 15/36] chore: Added events for build, namespace and orb --- cmd/build.go | 10 +++++++- cmd/namespace.go | 10 +++++++- cmd/orb.go | 6 +++++ cmd/orb_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++ telemetry/events.go | 39 +++++++++++++++++------------- 5 files changed, 104 insertions(+), 19 deletions(-) diff --git a/cmd/build.go b/cmd/build.go index c32ed969c..04ae56d43 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -1,8 +1,10 @@ package cmd import ( + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "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" ) @@ -16,7 +18,13 @@ func newLocalExecuteCommand(config *settings.Config) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, _ []string) error { - return local.Execute(cmd.Flags(), config, args) + telemetryClient := create_telemetry.CreateTelemetry(config) + defer telemetryClient.Close() + + err := local.Execute(cmd.Flags(), config, args) + _ = telemetryClient.Track(telemetry.CreateLocalExecuteEvent(create_telemetry.GetCommandInformation(cmd, true))) + + return err }, Args: cobra.MinimumNArgs(1), } diff --git a/cmd/namespace.go b/cmd/namespace.go index 0e5fb8cc5..e9c0bf912 100644 --- a/cmd/namespace.go +++ b/cmd/namespace.go @@ -6,8 +6,10 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/prompt" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/google/uuid" "github.com/spf13/cobra" ) @@ -68,13 +70,19 @@ Please note that at this time all namespaces created in the registry are world-r return validateToken(opts.cfg) }, RunE: func(cmd *cobra.Command, _ []string) error { + telemetryClient := create_telemetry.CreateTelemetry(config) + defer telemetryClient.Close() + if opts.integrationTesting { opts.tty = createNamespaceTestUI{ confirm: true, } } - return createNamespace(cmd, opts) + err := createNamespace(cmd, opts) + _ = telemetryClient.Track(telemetry.CreateNamespaceEvent(create_telemetry.GetCommandInformation(cmd, true))) + + return err }, Args: cobra.RangeArgs(1, 3), Annotations: make(map[string]string), diff --git a/cmd/orb.go b/cmd/orb.go index a7037c7d8..5134209b0 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -20,11 +20,13 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/collaborators" "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/filetree" "github.com/CircleCI-Public/circleci-cli/process" "github.com/CircleCI-Public/circleci-cli/prompt" "github.com/CircleCI-Public/circleci-cli/references" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/version" "github.com/fatih/color" "github.com/pkg/errors" @@ -388,6 +390,10 @@ Please note that at this time all orbs created in the registry are world-readabl opts.args = args opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) + telemetryClient := create_telemetry.CreateTelemetry(config) + defer telemetryClient.Close() + telemetryClient.Track(telemetry.CreateOrbEvent(create_telemetry.GetCommandInformation(cmd, true))) + // PersistentPreRunE overwrites the inherited persistent hook from rootCmd // So we explicitly call it here to retain that behavior. // As of writing this comment, that is only for daily update checks. diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 7b4e917ba..3f773b0c9 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "os" "os/exec" "path/filepath" "strconv" @@ -16,6 +17,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "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/ginkgo/extensions/table" . "github.com/onsi/gomega" @@ -23,6 +25,62 @@ import ( "github.com/onsi/gomega/gexec" ) +var _ = Describe("Orb telemetry", func() { + var ( + command *exec.Cmd + orb *clitest.TmpFile + tempSettings *clitest.TempSettings + telemetryDestFilePath string + ) + + BeforeEach(func() { + tempSettings = clitest.WithTempSettings() + telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") + orb = clitest.OpenTmpFile(tempSettings.Home, "orb.yml") + command = exec.Command(pathCLI, + "orb", "validate", orb.Path, + "--skip-update-check", + "--token", "token", + "--host", tempSettings.TestServer.URL(), + "--mock-telemetry", telemetryDestFilePath, + ) + }) + + AfterEach(func() { + orb.Close() + tempSettings.Close() + if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { + os.Remove(telemetryDestFilePath) + } + }) + + It("works", func() { + orb.Write([]byte(`{}`)) + + mockOrbIntrospection(true, "", tempSettings) + + tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + res.Write([]byte(`{"orbConfig": {"sourceYaml": "{}", "valid": true, "errors": []} }`)) + }) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + + clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + telemetry.CreateOrbEvent(telemetry.CommandInfo{ + Name: "validate", + LocalArgs: map[string]string{ + "org-slug": "", + "help": "false", + "org-id": "", + }, + }), + }) + }) +}) + var _ = Describe("Orb integration tests", func() { Describe("Orb help text", func() { It("shows a link to the docs", func() { diff --git a/telemetry/events.go b/telemetry/events.go index c6a20dbff..52620823c 100644 --- a/telemetry/events.go +++ b/telemetry/events.go @@ -12,12 +12,17 @@ type CommandInfo struct { LocalArgs map[string]string } -func localArgsToProperties(cmdInfo CommandInfo) map[string]interface{} { +func createEventFromCommandInfo(name string, cmdInfo CommandInfo) Event { properties := map[string]interface{}{} for key, value := range cmdInfo.LocalArgs { properties[fmt.Sprintf("cmd.flag.%s", key)] = value } - return properties + + return Event{ + Object: fmt.Sprintf("cli-%s", name), + Action: cmdInfo.Name, + Properties: properties, + } } func errorToProperties(err error) map[string]interface{} { @@ -48,11 +53,7 @@ func CreateVersionEvent(version string) Event { } func CreateUpdateEvent(cmdInfo CommandInfo) Event { - return Event{ - Object: "cli-update", - Action: cmdInfo.Name, - Properties: localArgsToProperties(cmdInfo), - } + return createEventFromCommandInfo("update", cmdInfo) } func CreateDiagnosticEvent(err error) Event { @@ -72,17 +73,21 @@ func CreateOpenEvent(err error) Event { } func CreateCompletionCommand(cmdInfo CommandInfo) Event { - return Event{ - Object: "cli-completion", - Action: cmdInfo.Name, - Properties: localArgsToProperties(cmdInfo), - } + return createEventFromCommandInfo("completion", cmdInfo) } func CreateConfigEvent(cmdInfo CommandInfo) Event { - return Event{ - Object: "cli-config", - Action: cmdInfo.Name, - Properties: localArgsToProperties(cmdInfo), - } + return createEventFromCommandInfo("config", cmdInfo) +} + +func CreateLocalExecuteEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("local", cmdInfo) +} + +func CreateNamespaceEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("namespace", cmdInfo) +} + +func CreateOrbEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("orb", cmdInfo) } From 051386b32668b4bef1e8d7988c8aed093851ce1f Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 11 Jul 2023 16:08:31 +0200 Subject: [PATCH 16/36] chore: Added events for policy --- cmd/policy/policy.go | 18 +++++++++++++++--- cmd/policy/policy_test.go | 11 ++++++----- telemetry/events.go | 4 ++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cmd/policy/policy.go b/cmd/policy/policy.go index 693bfb0c3..cbd246e57 100644 --- a/cmd/policy/policy.go +++ b/cmd/policy/policy.go @@ -24,17 +24,29 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/policy" "github.com/CircleCI-Public/circleci-cli/api/rest" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/config" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/CircleCI-Public/circleci-cli/settings" ) // NewCommand creates the root policy command with all policy subcommands attached. func NewCommand(globalConfig *settings.Config, preRunE validator.Validator) *cobra.Command { cmd := &cobra.Command{ - Use: "policy", - PersistentPreRunE: preRunE, - Short: "Manage security policies", + Use: "policy", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + telemetryClient := create_telemetry.CreateTelemetry(globalConfig) + defer telemetryClient.Close() + telemetryClient.Track(telemetry.CreatePolicyEvent(create_telemetry.GetCommandInformation(cmd, true))) + + if preRunE != nil { + return preRunE(cmd, args) + } + return nil + }, + Short: "Manage security policies", Long: `Policies ensures security of build configs via security policy management framework. This group of commands allows the management of polices to be verified against build configs.`, } diff --git a/cmd/policy/policy_test.go b/cmd/policy/policy_test.go index 39b414649..ac7fab59c 100644 --- a/cmd/policy/policy_test.go +++ b/cmd/policy/policy_test.go @@ -57,7 +57,7 @@ func TestPushPolicyWithPrompt(t *testing.T) { })) defer svr.Close() - config := &settings.Config{Token: "testtoken", HTTPClient: http.DefaultClient} + config := &settings.Config{Token: "testtoken", HTTPClient: http.DefaultClient, IsTelemetryDisabled: true} cmd := NewCommand(config, nil) buffer := makeSafeBuffer() @@ -1382,10 +1382,11 @@ func TestTestRunner(t *testing.T) { func makeCMD(circleHost string, token string) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) { config := &settings.Config{ - Host: circleHost, - Token: token, - RestEndpoint: "/api/v2", - HTTPClient: http.DefaultClient, + Host: circleHost, + Token: token, + RestEndpoint: "/api/v2", + HTTPClient: http.DefaultClient, + IsTelemetryDisabled: true, } cmd := NewCommand(config, nil) diff --git a/telemetry/events.go b/telemetry/events.go index 52620823c..4278968ee 100644 --- a/telemetry/events.go +++ b/telemetry/events.go @@ -91,3 +91,7 @@ func CreateNamespaceEvent(cmdInfo CommandInfo) Event { func CreateOrbEvent(cmdInfo CommandInfo) Event { return createEventFromCommandInfo("orb", cmdInfo) } + +func CreatePolicyEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("policy", cmdInfo) +} From 4837c9934bcd0fc321fb6fa83e780dde33fae7b2 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 11 Jul 2023 16:43:36 +0200 Subject: [PATCH 17/36] chore: Added events on runner --- cmd/runner/instance.go | 10 ++++++ cmd/runner/resource_class.go | 7 ++++ cmd/runner/resource_class_test.go | 3 +- cmd/runner/runner.go | 8 ++++- cmd/runner/telemetry_test.go | 56 +++++++++++++++++++++++++++++++ cmd/runner/token.go | 7 ++++ telemetry/events.go | 16 +++++++++ telemetry/telemetry.go | 4 +++ 8 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 cmd/runner/telemetry_test.go diff --git a/cmd/runner/instance.go b/cmd/runner/instance.go index 28f260937..9a2f869b5 100644 --- a/cmd/runner/instance.go +++ b/cmd/runner/instance.go @@ -8,7 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/CircleCI-Public/circleci-cli/api/runner" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" + "github.com/CircleCI-Public/circleci-cli/telemetry" ) func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command { @@ -26,6 +28,14 @@ func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra Args: cobra.ExactArgs(1), PreRunE: preRunE, RunE: func(_ *cobra.Command, args []string) error { + var err error + + telemetryClient := o.createTelemetry() + defer (func() { + _ = telemetryClient.Track(telemetry.CreateRunnerInstanceEvent(create_telemetry.GetCommandInformation(cmd, true), err)) + _ = telemetryClient.Close() + })() + runners, err := o.r.GetRunnerInstances(args[0]) if err != nil { return err diff --git a/cmd/runner/resource_class.go b/cmd/runner/resource_class.go index 1a727737e..3bb4e666a 100644 --- a/cmd/runner/resource_class.go +++ b/cmd/runner/resource_class.go @@ -7,13 +7,20 @@ import ( "github.com/spf13/cobra" "github.com/CircleCI-Public/circleci-cli/api/runner" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" + "github.com/CircleCI-Public/circleci-cli/telemetry" ) func newResourceClassCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command { cmd := &cobra.Command{ Use: "resource-class", Short: "Operate on runner resource-classes", + PersistentPreRun: func(cmd *cobra.Command, _ []string) { + telemetryClient := o.createTelemetry() + defer telemetryClient.Close() + telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(create_telemetry.GetCommandInformation(cmd, true))) + }, } genToken := false diff --git a/cmd/runner/resource_class_test.go b/cmd/runner/resource_class_test.go index df9cbbf27..cf1cd3390 100644 --- a/cmd/runner/resource_class_test.go +++ b/cmd/runner/resource_class_test.go @@ -12,11 +12,12 @@ import ( "gotest.tools/v3/assert/cmp" "github.com/CircleCI-Public/circleci-cli/api/runner" + "github.com/CircleCI-Public/circleci-cli/telemetry" ) func Test_ResourceClass(t *testing.T) { runner := runnerMock{} - cmd := newResourceClassCommand(&runnerOpts{r: &runner}, nil) + cmd := newResourceClassCommand(&runnerOpts{r: &runner, createTelemetry: telemetry.CreateNullClient}, nil) stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) cmd.SetOut(stdout) diff --git a/cmd/runner/runner.go b/cmd/runner/runner.go index 72424c1b6..1eb3d0d3d 100644 --- a/cmd/runner/runner.go +++ b/cmd/runner/runner.go @@ -7,12 +7,15 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/rest" "github.com/CircleCI-Public/circleci-cli/api/runner" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" ) type runnerOpts struct { - r running + r running + createTelemetry func() telemetry.Client } func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Command { @@ -28,6 +31,9 @@ func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Com host = config.Host } opts.r = runner.New(rest.NewFromConfig(host, config)) + opts.createTelemetry = func() telemetry.Client { + return create_telemetry.CreateTelemetry(config) + } }, } diff --git a/cmd/runner/telemetry_test.go b/cmd/runner/telemetry_test.go new file mode 100644 index 000000000..256b27076 --- /dev/null +++ b/cmd/runner/telemetry_test.go @@ -0,0 +1,56 @@ +package runner + +import ( + "bytes" + "testing" + + "github.com/CircleCI-Public/circleci-cli/telemetry" + "gotest.tools/v3/assert" +) + +type testTelemetryClient struct { + events []telemetry.Event +} + +func (c *testTelemetryClient) Track(event telemetry.Event) error { + c.events = append(c.events, event) + return nil +} + +func (c *testTelemetryClient) Close() error { return nil } + +func Test_RunnerTelemetry(t *testing.T) { + t.Run("resource-class", func(t *testing.T) { + telemetryClient := &testTelemetryClient{make([]telemetry.Event, 0)} + runner := runnerMock{} + cmd := newResourceClassCommand(&runnerOpts{r: &runner, createTelemetry: func() telemetry.Client { return telemetryClient }}, nil) + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + cmd.SetOut(stdout) + cmd.SetErr(stderr) + + defer runner.reset() + defer stdout.Reset() + defer stderr.Reset() + + cmd.SetArgs([]string{ + "create", + "my-namespace/my-other-resource-class", + "my-description", + "--generate-token", + }) + + err := cmd.Execute() + assert.NilError(t, err) + + assert.DeepEqual(t, telemetryClient.events, []telemetry.Event{ + telemetry.CreateRunnerResourceClassEvent(telemetry.CommandInfo{ + Name: "create", + LocalArgs: map[string]string{ + "generate-token": "true", + "help": "false", + }, + }), + }) + }) +} diff --git a/cmd/runner/token.go b/cmd/runner/token.go index 4771b37e9..d59c003f8 100644 --- a/cmd/runner/token.go +++ b/cmd/runner/token.go @@ -3,7 +3,9 @@ package runner import ( "time" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -12,6 +14,11 @@ func newTokenCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command cmd := &cobra.Command{ Use: "token", Short: "Operate on runner tokens", + PersistentPreRun: func(cmd *cobra.Command, _ []string) { + telemetryClient := o.createTelemetry() + defer telemetryClient.Close() + telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(create_telemetry.GetCommandInformation(cmd, true))) + }, } cmd.AddCommand(&cobra.Command{ diff --git a/telemetry/events.go b/telemetry/events.go index 4278968ee..1e52a4750 100644 --- a/telemetry/events.go +++ b/telemetry/events.go @@ -95,3 +95,19 @@ func CreateOrbEvent(cmdInfo CommandInfo) Event { func CreatePolicyEvent(cmdInfo CommandInfo) Event { return createEventFromCommandInfo("policy", cmdInfo) } + +func CreateRunnerInstanceEvent(cmdInfo CommandInfo, err error) Event { + event := createEventFromCommandInfo("runner-instance", cmdInfo) + if err != nil { + event.Properties["error"] = err.Error() + } + return event +} + +func CreateRunnerResourceClassEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("runner-resource-class", cmdInfo) +} + +func CreateRunnerToken(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("runner-resource-class", cmdInfo) +} diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index db3e24200..c4c0c89f9 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -68,6 +68,10 @@ func SendTelemetryApproval(user User, approval Approval) error { // Null client // Used when telemetry is disabled +func CreateNullClient() Client { + return nullClient{} +} + type nullClient struct{} func (cli nullClient) Close() error { return nil } From 7f0184b6a5b2c63117f489a5c74966ccce253b09 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 11 Jul 2023 17:58:42 +0200 Subject: [PATCH 18/36] chore: Improve telemetry handling for config commands --- cmd/config.go | 7 +++---- cmd/config_test.go | 2 +- telemetry/events.go | 8 ++++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index ea680a690..7465e20bf 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -56,7 +56,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { defer closeTelemetryClient() err := packConfig(args) - telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true))) + _ = telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) return err }, Args: cobra.ExactArgs(1), @@ -89,7 +89,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { IgnoreDeprecatedImages: ignoreDeprecatedImages, VerboseOutput: verboseOutput, }) - telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true))) + telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) return err }, @@ -124,7 +124,6 @@ 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, @@ -132,7 +131,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { PipelineParamsFilePath: pipelineParamsFilePath, VerboseOutput: verboseOutput, }) - telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true))) + telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) if err != nil { return err } diff --git a/cmd/config_test.go b/cmd/config_test.go index b48770cdf..f0efe6ecd 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -61,7 +61,7 @@ var _ = Describe("Config", func() { telemetry.CreateConfigEvent(telemetry.CommandInfo{ Name: "pack", LocalArgs: map[string]string{"help": "false"}, - }), + }, nil), }) }) }) diff --git a/telemetry/events.go b/telemetry/events.go index 1e52a4750..eb923d1a1 100644 --- a/telemetry/events.go +++ b/telemetry/events.go @@ -76,8 +76,12 @@ func CreateCompletionCommand(cmdInfo CommandInfo) Event { return createEventFromCommandInfo("completion", cmdInfo) } -func CreateConfigEvent(cmdInfo CommandInfo) Event { - return createEventFromCommandInfo("config", cmdInfo) +func CreateConfigEvent(cmdInfo CommandInfo, err error) Event { + event := createEventFromCommandInfo("config", cmdInfo) + if err != nil { + event.Properties["error"] = err.Error() + } + return event } func CreateLocalExecuteEvent(cmdInfo CommandInfo) Event { From 7e7f1ee2523143afa2cb9c7a36aa552070319d28 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 11 Jul 2023 18:20:21 +0200 Subject: [PATCH 19/36] fix: Version still run if you have no telemetry --- cmd/version.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cmd/version.go b/cmd/version.go index 7c6f5d058..8470333db 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -26,6 +26,9 @@ func newVersionCommand(config *settings.Config) *cobra.Command { Short: "Display version information", PersistentPreRun: func(_ *cobra.Command, _ []string) { telemetryClient = create_telemetry.CreateTelemetry(config) + defer telemetryClient.Close() + _ = telemetryClient.Track(telemetry.CreateVersionEvent(version.Version)) + opts.cfg.SkipUpdateCheck = true }, PreRun: func(cmd *cobra.Command, args []string) { @@ -34,11 +37,5 @@ 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(version.Version)); err != nil { - return err - } - return telemetryClient.Close() - }, } } From dbd1239c145016f7c3d6241543cc9c34f4b407ce Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Wed, 12 Jul 2023 14:55:37 +0200 Subject: [PATCH 20/36] inject Segment information at build time --- Taskfile.yml | 9 +++++---- telemetry/telemetry.go | 32 +++++++++++++++++++------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index ad2d19df9..f261e03a9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -47,14 +47,15 @@ tasks: build: desc: Build main cmds: - - go build -v -o build/darwin/amd64/circleci . - + # LDFlags sets the segment endpoint to an empty string so that all it is replaced by the default value + - go build -v -o build/darwin/amd64/circleci -ldflags="-X 'github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=' -X 'github.com/CircleCI-Public/circleci-cli/telemetry.SegmentKey=AbgkrgN4cbRhAVEwlzMkHbwvrXnxHh35'" . + 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 ./... \ No newline at end of file + - TESTING=true go test -race -coverprofile=coverage.txt ./... diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index c4c0c89f9..59a0298dd 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -9,13 +9,16 @@ import ( "github.com/segmentio/analytics-go" ) -type Approval string - var ( // Overwrite this function for tests CreateActiveTelemetry = newSegmentClient + + SegmentEndpoint = "localhost" + SegmentKey = "" ) +type Approval string + const ( Enabled Approval = "enabled" Disabled Approval = "disabled" @@ -86,25 +89,28 @@ type segmentClient struct { user User } -const ( - segmentKey = "" -) - func newSegmentClient(user User) Client { - cli := analytics.New(segmentKey) + cli, err := analytics.NewWithConfig(SegmentKey, analytics.Config{ + Endpoint: SegmentEndpoint, + }) - userID := user.UniqueID - if userID == "" { - userID = "none" + if err != nil { + return CreateNullClient() } - err := cli.Enqueue( + if len(user.UniqueID) == 0 { + user.UniqueID = "null" + } + + err = cli.Enqueue( analytics.Identify{ - UserId: userID, + UserId: user.UniqueID, Traits: analytics.NewTraits().Set("os", user.OS), }, ) - fmt.Printf("Error while identifying with telemetry: %s\n", err) + if err != nil { + fmt.Printf("Error while identifying with telemetry: %s\n", err) + } return &segmentClient{cli, user} } From 7d36b51fe1c476711bb1aba9d0b78311ec1032d2 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Wed, 12 Jul 2023 15:17:21 +0200 Subject: [PATCH 21/36] test: Improve the way telemetry is tested --- clitest/clitest.go | 15 ++++++++++----- clitest/telemetry.go | 4 ++-- cmd/config_test.go | 11 ++--------- cmd/diagnostic_test.go | 22 ++++++++++------------ cmd/namespace_test.go | 36 +++++++++++++++++++++++++++++++++--- cmd/orb_test.go | 16 +++++----------- cmd/setup_test.go | 14 ++++---------- cmd/update_test.go | 24 ++++-------------------- 8 files changed, 70 insertions(+), 72 deletions(-) diff --git a/clitest/clitest.go b/clitest/clitest.go index ae7bd8033..c0bb3c1ce 100644 --- a/clitest/clitest.go +++ b/clitest/clitest.go @@ -32,11 +32,12 @@ func ShouldFail() types.GomegaMatcher { // TempSettings contains useful settings for testing the CLI type TempSettings struct { - Home string - TestServer *ghttp.Server - Config *TmpFile - Telemetry *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 @@ -44,6 +45,9 @@ func (settings *TempSettings) Close() error { settings.TestServer.Close() settings.Config.Close() settings.Update.Close() + if _, err := os.Stat(settings.TelemetryDestPath); err == nil || !os.IsNotExist(err) { + os.Remove(settings.TelemetryDestPath) + } return os.RemoveAll(settings.Home) } @@ -79,6 +83,7 @@ func WithTempSettings() *TempSettings { 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") diff --git a/clitest/telemetry.go b/clitest/telemetry.go index cb7dee2ce..fc21f85ce 100644 --- a/clitest/telemetry.go +++ b/clitest/telemetry.go @@ -8,8 +8,8 @@ import ( "github.com/onsi/gomega" ) -func CompareTelemetryEvent(filePath string, expected []telemetry.Event) { - content, err := os.ReadFile(filePath) +func CompareTelemetryEvent(settings *TempSettings, expected []telemetry.Event) { + content, err := os.ReadFile(settings.TelemetryDestPath) gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) result := []telemetry.Event{} diff --git a/cmd/config_test.go b/cmd/config_test.go index f0efe6ecd..7000f0cfa 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -31,25 +31,18 @@ var _ = Describe("Config", func() { }) 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, + "--mock-telemetry", tempSettings.TelemetryDestPath, ) }) AfterEach(func() { tempSettings.Close() - if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { - os.Remove(telemetryDestFilePath) - } }) It("should send telemetry event", func() { @@ -57,7 +50,7 @@ var _ = Describe("Config", func() { Expect(err).ShouldNot(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) - clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ telemetry.CreateConfigEvent(telemetry.CommandInfo{ Name: "pack", LocalArgs: map[string]string{"help": "false"}, diff --git a/cmd/diagnostic_test.go b/cmd/diagnostic_test.go index d3ec07f94..a298c9ab5 100644 --- a/cmd/diagnostic_test.go +++ b/cmd/diagnostic_test.go @@ -3,7 +3,6 @@ package cmd_test import ( "fmt" "net/http" - "os" "os/exec" "path/filepath" @@ -19,21 +18,18 @@ import ( var _ = Describe("Diagnostic", func() { var ( - telemetryDestFilePath string - tempSettings *clitest.TempSettings - command *exec.Cmd - defaultEndpoint = "graphql-unstable" + 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(), - "--mock-telemetry", telemetryDestFilePath) + "--host", tempSettings.TestServer.URL()) query := `query IntrospectionQuery { __schema { @@ -82,19 +78,21 @@ 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() { + command = commandWithHome(pathCLI, tempSettings.Home, + "diagnostic", + "--skip-update-check", + "--host", tempSettings.TestServer.URL(), + "--mock-telemetry", tempSettings.TelemetryDestPath) 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{ + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ telemetry.CreateDiagnosticEvent(nil), }) }) diff --git a/cmd/namespace_test.go b/cmd/namespace_test.go index 189e4901e..8749be11a 100644 --- a/cmd/namespace_test.go +++ b/cmd/namespace_test.go @@ -4,8 +4,10 @@ import ( "fmt" "net/http" "os/exec" + "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/gbytes" @@ -14,9 +16,10 @@ import ( var _ = Describe("Namespace integration tests", func() { var ( - tempSettings *clitest.TempSettings - token string = "testtoken" - command *exec.Cmd + tempSettings *clitest.TempSettings + token string = "testtoken" + command *exec.Cmd + telemetryDestFilePath string ) BeforeEach(func() { @@ -27,6 +30,33 @@ var _ = Describe("Namespace integration tests", func() { tempSettings.Close() }) + Describe("telemetry", func() { + It("sends expected event", func() { + telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") + command = exec.Command(pathCLI, + "namespace", "create", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--integration-testing", + "foo-ns", + "--org-id", `"bb604b45-b6b0-4b81-ad80-796f15eddf87"`, + "--mock-telemetry", telemetryDestFilePath, + ) + + tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + res.Write([]byte(`{"organization":{name:"test-org","id":"org-id"}}`)) + }) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{}) + }) + }) + Context("create, with interactive prompts", func() { Describe("registering a namespace with orgID", func() { BeforeEach(func() { diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 3f773b0c9..2c80d86b1 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net/http" - "os" "os/exec" "path/filepath" "strconv" @@ -27,31 +26,26 @@ import ( var _ = Describe("Orb telemetry", func() { var ( - command *exec.Cmd - orb *clitest.TmpFile - tempSettings *clitest.TempSettings - telemetryDestFilePath string + command *exec.Cmd + orb *clitest.TmpFile + tempSettings *clitest.TempSettings ) BeforeEach(func() { tempSettings = clitest.WithTempSettings() - telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") orb = clitest.OpenTmpFile(tempSettings.Home, "orb.yml") command = exec.Command(pathCLI, "orb", "validate", orb.Path, "--skip-update-check", "--token", "token", "--host", tempSettings.TestServer.URL(), - "--mock-telemetry", telemetryDestFilePath, + "--mock-telemetry", tempSettings.TelemetryDestPath, ) }) AfterEach(func() { orb.Close() tempSettings.Close() - if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { - os.Remove(telemetryDestFilePath) - } }) It("works", func() { @@ -68,7 +62,7 @@ var _ = Describe("Orb telemetry", func() { Expect(err).ShouldNot(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) - clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ telemetry.CreateOrbEvent(telemetry.CommandInfo{ Name: "validate", LocalArgs: map[string]string{ diff --git a/cmd/setup_test.go b/cmd/setup_test.go index 329077096..c7c38e10e 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -5,7 +5,6 @@ import ( "io" "os" "os/exec" - "path/filepath" "regexp" "runtime" @@ -19,27 +18,22 @@ import ( var _ = Describe("Setup telemetry", func() { var ( - command *exec.Cmd - tempSettings *clitest.TempSettings - telemetryDestFilePath string + command *exec.Cmd + tempSettings *clitest.TempSettings ) 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, + "--mock-telemetry", tempSettings.TelemetryDestPath, ) }) AfterEach(func() { tempSettings.Close() - if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { - os.Remove(telemetryDestFilePath) - } }) It("should send telemetry event", func() { @@ -47,7 +41,7 @@ var _ = Describe("Setup telemetry", func() { Expect(err).ShouldNot(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) - clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ telemetry.CreateSetupEvent(false), }) }) diff --git a/cmd/update_test.go b/cmd/update_test.go index 238b32ef3..ee37f17dd 100644 --- a/cmd/update_test.go +++ b/cmd/update_test.go @@ -2,7 +2,6 @@ package cmd_test import ( "net/http" - "os" "os/exec" "path/filepath" @@ -73,32 +72,17 @@ var _ = Describe("Update", func() { }) Describe("telemetry", func() { - var ( - telemetryDestFilePath string - ) - - BeforeEach(func() { - telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") - }) - - AfterEach(func() { - tempSettings.Close() - if _, err := os.Stat(telemetryDestFilePath); err == nil || !os.IsNotExist(err) { - os.Remove(telemetryDestFilePath) - } - }) - It("should send telemetry event when calling parent command", func() { command = exec.Command(pathCLI, "update", "--github-api", tempSettings.TestServer.URL(), - "--mock-telemetry", telemetryDestFilePath, + "--mock-telemetry", tempSettings.TelemetryDestPath, ) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) - clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ telemetry.CreateUpdateEvent(telemetry.CommandInfo{ Name: "update", LocalArgs: map[string]string{ @@ -114,13 +98,13 @@ var _ = Describe("Update", func() { "update", "check", "--github-api", tempSettings.TestServer.URL(), - "--mock-telemetry", telemetryDestFilePath, + "--mock-telemetry", tempSettings.TelemetryDestPath, ) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) - clitest.CompareTelemetryEvent(telemetryDestFilePath, []telemetry.Event{ + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ telemetry.CreateUpdateEvent(telemetry.CommandInfo{ Name: "check", LocalArgs: map[string]string{ From 32612455789f96e117f88cd46acfec7e0bef98f7 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Wed, 12 Jul 2023 15:55:03 +0200 Subject: [PATCH 22/36] minor improvements --- clitest/clitest.go | 3 --- cmd/config_test.go | 4 ++-- cmd/namespace_test.go | 14 ++++++++++++-- cmd/testdata/config_validate/config.yml | 16 ---------------- 4 files changed, 14 insertions(+), 23 deletions(-) delete mode 100644 cmd/testdata/config_validate/config.yml diff --git a/clitest/clitest.go b/clitest/clitest.go index c0bb3c1ce..16fe54085 100644 --- a/clitest/clitest.go +++ b/clitest/clitest.go @@ -45,9 +45,6 @@ func (settings *TempSettings) Close() error { settings.TestServer.Close() settings.Config.Close() settings.Update.Close() - if _, err := os.Stat(settings.TelemetryDestPath); err == nil || !os.IsNotExist(err) { - os.Remove(settings.TelemetryDestPath) - } return os.RemoveAll(settings.Home) } diff --git a/cmd/config_test.go b/cmd/config_test.go index 7000f0cfa..7e2e41e80 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -36,7 +36,7 @@ var _ = Describe("Config", func() { command = commandWithHome(pathCLI, tempSettings.Home, "config", "pack", "--skip-update-check", - "testdata/hugo-pack/.circleci", + filepath.Join("testdata", "hugo-pack", ".circleci"), "--mock-telemetry", tempSettings.TelemetryDestPath, ) }) @@ -64,7 +64,7 @@ var _ = Describe("Config", 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")) }) diff --git a/cmd/namespace_test.go b/cmd/namespace_test.go index 8749be11a..03cf5daeb 100644 --- a/cmd/namespace_test.go +++ b/cmd/namespace_test.go @@ -46,14 +46,24 @@ var _ = Describe("Namespace integration tests", func() { tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusOK) - res.Write([]byte(`{"organization":{name:"test-org","id":"org-id"}}`)) + res.Write([]byte(`{"data":{"organization":{"name":"test-org","id":"bb604b45-b6b0-4b81-ad80-796f15eddf87"}}}`)) }) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) - clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{}) + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateNamespaceEvent(telemetry.CommandInfo{ + Name: "create", + LocalArgs: map[string]string{ + "help": "false", + "integration-testing": "true", + "no-prompt": "false", + "org-id": "\"bb604b45-b6b0-4b81-ad80-796f15eddf87\"", + }, + }), + }) }) }) diff --git a/cmd/testdata/config_validate/config.yml b/cmd/testdata/config_validate/config.yml deleted file mode 100644 index e6e7a4435..000000000 --- a/cmd/testdata/config_validate/config.yml +++ /dev/null @@ -1,16 +0,0 @@ -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 From 25c6676c3c97089bdb3e8c6237a4b908f8887052 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Wed, 12 Jul 2023 16:30:24 +0200 Subject: [PATCH 23/36] fix lint --- cmd/completion.go | 2 +- cmd/config.go | 4 ++-- cmd/diagnostic.go | 2 +- cmd/follow.go | 2 +- cmd/namespace_test.go | 2 +- cmd/orb.go | 2 +- cmd/orb_test.go | 2 +- cmd/policy/policy.go | 2 +- cmd/runner/resource_class.go | 2 +- cmd/runner/token.go | 2 +- cmd/update.go | 2 +- cmd/update_test.go | 20 +++++++++++++++++++- telemetry/telemetry.go | 2 +- 13 files changed, 32 insertions(+), 14 deletions(-) diff --git a/cmd/completion.go b/cmd/completion.go index 43b6aa948..d2f1e2aab 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -16,7 +16,7 @@ func newCompletionCommand(config *settings.Config) *cobra.Command { PersistentPreRun: func(cmd *cobra.Command, _ []string) { telemetryClient := create_telemetry.CreateTelemetry(config) defer telemetryClient.Close() - telemetryClient.Track(telemetry.CreateCompletionCommand(create_telemetry.GetCommandInformation(cmd, false))) + _ = telemetryClient.Track(telemetry.CreateCompletionCommand(create_telemetry.GetCommandInformation(cmd, false))) }, Run: func(cmd *cobra.Command, _ []string) { err := cmd.Help() diff --git a/cmd/config.go b/cmd/config.go index 7465e20bf..fddb791a3 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -89,7 +89,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { IgnoreDeprecatedImages: ignoreDeprecatedImages, VerboseOutput: verboseOutput, }) - telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) + _ = telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) return err }, @@ -131,7 +131,7 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { PipelineParamsFilePath: pipelineParamsFilePath, VerboseOutput: verboseOutput, }) - telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) + _ = telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) if err != nil { return err } diff --git a/cmd/diagnostic.go b/cmd/diagnostic.go index 898c97222..44784c2ff 100644 --- a/cmd/diagnostic.go +++ b/cmd/diagnostic.go @@ -35,7 +35,7 @@ func newDiagnosticCommand(config *settings.Config) *cobra.Command { defer telemetryClient.Close() err := diagnostic(opts) - telemetryClient.Track(telemetry.CreateDiagnosticEvent(err)) + _ = telemetryClient.Track(telemetry.CreateDiagnosticEvent(err)) return err }, diff --git a/cmd/follow.go b/cmd/follow.go index 89005badd..9eac8b942 100644 --- a/cmd/follow.go +++ b/cmd/follow.go @@ -60,7 +60,7 @@ func followProjectCommand(config *settings.Config) *cobra.Command { defer telemetryClient.Close() err := followProject(opts) - telemetryClient.Track(telemetry.CreateFollowEvent(err)) + _ = telemetryClient.Track(telemetry.CreateFollowEvent(err)) return err }, diff --git a/cmd/namespace_test.go b/cmd/namespace_test.go index 03cf5daeb..06fff16e1 100644 --- a/cmd/namespace_test.go +++ b/cmd/namespace_test.go @@ -46,7 +46,7 @@ var _ = Describe("Namespace integration tests", func() { tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusOK) - res.Write([]byte(`{"data":{"organization":{"name":"test-org","id":"bb604b45-b6b0-4b81-ad80-796f15eddf87"}}}`)) + _, _ = res.Write([]byte(`{"data":{"organization":{"name":"test-org","id":"bb604b45-b6b0-4b81-ad80-796f15eddf87"}}}`)) }) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) diff --git a/cmd/orb.go b/cmd/orb.go index 5134209b0..f8443706b 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -392,7 +392,7 @@ Please note that at this time all orbs created in the registry are world-readabl telemetryClient := create_telemetry.CreateTelemetry(config) defer telemetryClient.Close() - telemetryClient.Track(telemetry.CreateOrbEvent(create_telemetry.GetCommandInformation(cmd, true))) + _ = telemetryClient.Track(telemetry.CreateOrbEvent(create_telemetry.GetCommandInformation(cmd, true))) // PersistentPreRunE overwrites the inherited persistent hook from rootCmd // So we explicitly call it here to retain that behavior. diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 2c80d86b1..cd0741144 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -55,7 +55,7 @@ var _ = Describe("Orb telemetry", func() { tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusOK) - res.Write([]byte(`{"orbConfig": {"sourceYaml": "{}", "valid": true, "errors": []} }`)) + _, _ = res.Write([]byte(`{"orbConfig": {"sourceYaml": "{}", "valid": true, "errors": []} }`)) }) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) diff --git a/cmd/policy/policy.go b/cmd/policy/policy.go index cbd246e57..be13a0ae4 100644 --- a/cmd/policy/policy.go +++ b/cmd/policy/policy.go @@ -39,7 +39,7 @@ func NewCommand(globalConfig *settings.Config, preRunE validator.Validator) *cob PersistentPreRunE: func(cmd *cobra.Command, args []string) error { telemetryClient := create_telemetry.CreateTelemetry(globalConfig) defer telemetryClient.Close() - telemetryClient.Track(telemetry.CreatePolicyEvent(create_telemetry.GetCommandInformation(cmd, true))) + _ = telemetryClient.Track(telemetry.CreatePolicyEvent(create_telemetry.GetCommandInformation(cmd, true))) if preRunE != nil { return preRunE(cmd, args) diff --git a/cmd/runner/resource_class.go b/cmd/runner/resource_class.go index 3bb4e666a..4cfad2d12 100644 --- a/cmd/runner/resource_class.go +++ b/cmd/runner/resource_class.go @@ -19,7 +19,7 @@ func newResourceClassCommand(o *runnerOpts, preRunE validator.Validator) *cobra. PersistentPreRun: func(cmd *cobra.Command, _ []string) { telemetryClient := o.createTelemetry() defer telemetryClient.Close() - telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(create_telemetry.GetCommandInformation(cmd, true))) + _ = telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(create_telemetry.GetCommandInformation(cmd, true))) }, } diff --git a/cmd/runner/token.go b/cmd/runner/token.go index d59c003f8..2a249c5a4 100644 --- a/cmd/runner/token.go +++ b/cmd/runner/token.go @@ -17,7 +17,7 @@ func newTokenCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command PersistentPreRun: func(cmd *cobra.Command, _ []string) { telemetryClient := o.createTelemetry() defer telemetryClient.Close() - telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(create_telemetry.GetCommandInformation(cmd, true))) + _ = telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(create_telemetry.GetCommandInformation(cmd, true))) }, } diff --git a/cmd/update.go b/cmd/update.go index d11941082..a326fbbb2 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -34,7 +34,7 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { opts.cfg.SkipUpdateCheck = true telemetryClient := create_telemetry.CreateTelemetry(config) defer telemetryClient.Close() - telemetryClient.Track(telemetry.CreateUpdateEvent(create_telemetry.GetCommandInformation(cmd, cmd.Name() != "update"))) + _ = telemetryClient.Track(telemetry.CreateUpdateEvent(create_telemetry.GetCommandInformation(cmd, cmd.Name() != "update"))) }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args diff --git a/cmd/update_test.go b/cmd/update_test.go index ee37f17dd..97fec68bc 100644 --- a/cmd/update_test.go +++ b/cmd/update_test.go @@ -73,11 +73,29 @@ var _ = Describe("Update", func() { Describe("telemetry", func() { It("should send telemetry event when calling parent command", func() { - command = exec.Command(pathCLI, + updateCLI, err := gexec.Build("github.com/CircleCI-Public/circleci-cli") + Expect(err).ShouldNot(HaveOccurred()) + + command = exec.Command(updateCLI, "update", "--github-api", tempSettings.TestServer.URL(), "--mock-telemetry", tempSettings.TelemetryDestPath, ) + + assetBytes := golden.Get(GinkgoT(), filepath.FromSlash("update/foo.zip")) + assetResponse := string(assetBytes) + + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest(http.MethodGet, "/repos/CircleCI-Public/circleci-cli/releases"), + ghttp.RespondWith(http.StatusOK, response), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest(http.MethodGet, "/repos/CircleCI-Public/circleci-cli/releases/assets/1"), + ghttp.RespondWith(http.StatusOK, assetResponse), + ), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 59a0298dd..906ef1003 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -13,7 +13,7 @@ var ( // Overwrite this function for tests CreateActiveTelemetry = newSegmentClient - SegmentEndpoint = "localhost" + SegmentEndpoint = "http://localhost" SegmentKey = "" ) From f79234b70fddf4b347487898e67a1a0cff71e244 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Thu, 13 Jul 2023 09:51:44 +0200 Subject: [PATCH 24/36] fix: Added event for info command --- cmd/info/info.go | 18 ++++++++++++--- cmd/info/info_test.go | 51 ++++++++++++++++++++++++++++++++++++++++--- telemetry/events.go | 8 +++++++ 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/cmd/info/info.go b/cmd/info/info.go index 1ed3028fe..ade982b97 100644 --- a/cmd/info/info.go +++ b/cmd/info/info.go @@ -2,8 +2,10 @@ package info import ( "github.com/CircleCI-Public/circleci-cli/api/info" + "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" @@ -11,8 +13,9 @@ import ( // infoOptions info command options type infoOptions struct { - cfg *settings.Config - validator validator.Validator + cfg *settings.Config + validator validator.Validator + createTelemetry func() telemetry.Client } // NewInfoCommand information cobra command creation @@ -22,6 +25,9 @@ func NewInfoCommand(config *settings.Config, preRunE validator.Validator) *cobra opts := infoOptions{ cfg: config, validator: preRunE, + createTelemetry: func() telemetry.Client { + return create_telemetry.CreateTelemetry(config) + }, } infoCommand := &cobra.Command{ Use: "info", @@ -41,7 +47,13 @@ func orgInfoCommand(client info.InfoClient, opts infoOptions) *cobra.Command { Long: `View your Organizations' names and ids.`, PreRunE: opts.validator, RunE: func(cmd *cobra.Command, _ []string) error { - return getOrgInformation(cmd, client) + telemetryClient := opts.createTelemetry() + defer telemetryClient.Close() + + err := getOrgInformation(cmd, client) + _ = telemetryClient.Track(telemetry.CreateInfoEvent(create_telemetry.GetCommandInformation(cmd, true), err)) + + return err }, Annotations: make(map[string]string), Example: `circleci info org`, diff --git a/cmd/info/info_test.go b/cmd/info/info_test.go index 19bda1417..3ed51bdba 100644 --- a/cmd/info/info_test.go +++ b/cmd/info/info_test.go @@ -2,13 +2,17 @@ package info import ( "bytes" + "encoding/json" "fmt" "net/http" "net/http/httptest" + "os" "testing" + "github.com/CircleCI-Public/circleci-cli/clitest" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" "gotest.tools/v3/assert" ) @@ -106,6 +110,46 @@ func TestFailedValidator(t *testing.T) { assert.Error(t, err, errorMessage) } +func TestTelemetry(t *testing.T) { + tempSettings := clitest.WithTempSettings() + defer tempSettings.Close() + + // Test server + var serverHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[{"id":"id", "name":"name"}]`)) + } + server := httptest.NewServer(serverHandler) + defer server.Close() + + // Test command + config := &settings.Config{ + Token: "testtoken", + HTTPClient: http.DefaultClient, + Host: server.URL, + MockTelemetry: tempSettings.TelemetryDestPath, + } + cmd := NewInfoCommand(config, nil) + + // Execute + err := cmd.Execute() + + assert.NilError(t, err) + // We compare the + content, err := os.ReadFile(tempSettings.TelemetryDestPath) + assert.NilError(t, err) + + result := []telemetry.Event{} + err = json.Unmarshal(content, &result) + assert.NilError(t, err) + assert.DeepEqual(t, result, []telemetry.Event{ + telemetry.CreateInfoEvent(telemetry.CommandInfo{ + Name: "org", + LocalArgs: map[string]string{"help": "false"}, + }, nil), + }) +} + func defaultValidator(cmd *cobra.Command, args []string) error { return nil } @@ -115,9 +159,10 @@ func scaffoldCMD( validator validator.Validator, ) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) { config := &settings.Config{ - Token: "testtoken", - HTTPClient: http.DefaultClient, - Host: baseURL, + Token: "testtoken", + HTTPClient: http.DefaultClient, + Host: baseURL, + IsTelemetryDisabled: true, } cmd := NewInfoCommand(config, validator) diff --git a/telemetry/events.go b/telemetry/events.go index eb923d1a1..5e8362171 100644 --- a/telemetry/events.go +++ b/telemetry/events.go @@ -115,3 +115,11 @@ func CreateRunnerResourceClassEvent(cmdInfo CommandInfo) Event { func CreateRunnerToken(cmdInfo CommandInfo) Event { return createEventFromCommandInfo("runner-resource-class", cmdInfo) } + +func CreateInfoEvent(cmdInfo CommandInfo, err error) Event { + event := createEventFromCommandInfo("info", cmdInfo) + if err != nil { + event.Properties["error"] = err.Error() + } + return event +} From c357de6c92718172ea0d48fbadb328dfb96049db Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Thu, 13 Jul 2023 18:52:18 +0200 Subject: [PATCH 25/36] addressed minor pr comments --- Taskfile.yml | 5 +++-- clitest/clitest.go | 2 +- cmd/create_telemetry/create_telemetry.go | 12 ++++++------ cmd/create_telemetry/create_telemetry_test.go | 6 +++--- cmd/telemetry.go | 2 +- cmd/telemetry_test.go | 12 ++++++------ cmd/version.go | 1 + settings/settings.go | 2 +- telemetry/telemetry.go | 8 ++++---- 9 files changed, 26 insertions(+), 24 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index f261e03a9..2cbce4aae 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -47,8 +47,9 @@ tasks: build: desc: Build main cmds: - # LDFlags sets the segment endpoint to an empty string so that all it is replaced by the default value - - go build -v -o build/darwin/amd64/circleci -ldflags="-X 'github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=' -X 'github.com/CircleCI-Public/circleci-cli/telemetry.SegmentKey=AbgkrgN4cbRhAVEwlzMkHbwvrXnxHh35'" . + # 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=' -X 'github.com/CircleCI-Public/circleci-cli/telemetry.SegmentKey="`echo ${SEGMENT_KEY}`"'" . build-linux: desc: Build main diff --git a/clitest/clitest.go b/clitest/clitest.go index 16fe54085..24b0b25bc 100644 --- a/clitest/clitest.go +++ b/clitest/clitest.go @@ -74,7 +74,7 @@ func WithTempSettings() *TempSettings { tempSettings.Config = OpenTmpFile(settingsPath, "cli.yml") tempSettings.Telemetry = OpenTmpFile(settingsPath, "telemetry.yml") content, err := yaml.Marshal(settings.TelemetrySettings{ - IsActive: false, + IsEnabled: false, HasAnsweredPrompt: true, }) gomega.Expect(err).ToNot(gomega.HaveOccurred()) diff --git a/cmd/create_telemetry/create_telemetry.go b/cmd/create_telemetry/create_telemetry.go index 99b98d4a5..6425df310 100644 --- a/cmd/create_telemetry/create_telemetry.go +++ b/cmd/create_telemetry/create_telemetry.go @@ -87,7 +87,7 @@ func CreateTelemetry(config *settings.Config) telemetry.Client { } loadTelemetrySettings(&telemetrySettings, &user, apiClient, ui) - client := telemetry.CreateClient(user, telemetrySettings.IsActive) + client := telemetry.CreateClient(user, telemetrySettings.IsEnabled) return client } @@ -104,7 +104,7 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry // If we already have telemetry information or that telemetry is explicitly disabled, skip if settings.HasAnsweredPrompt { // If we have no user id, we try requesting the user id again - if settings.UserID == "" && settings.IsActive { + if settings.UserID == "" && settings.IsEnabled { myID, err := apiClient.GetMyUserId() if err == nil { settings.UserID = myID @@ -120,7 +120,7 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry // If stdin is not available, send telemetry event, disable telemetry and return if !isStdinATTY { - settings.IsActive = false + settings.IsEnabled = false err := telemetry.SendTelemetryApproval(telemetry.User{ UniqueID: settings.UniqueID, }, telemetry.NoStdin) @@ -136,11 +136,11 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry fmt.Println("Participation is voluntary, and your choice can be changed at any time through the command `cli telemetry enable` and `cli telemetry disable`.") fmt.Println("For more information, please see our privacy policy at https://circleci.com/legal/privacy/.") fmt.Println("") - settings.IsActive = ui.AskUserToApproveTelemetry("Enable telemetry?") + settings.IsEnabled = ui.AskUserToApproveTelemetry("Enable telemetry?") settings.HasAnsweredPrompt = true // Make sure we have user info and set them - if settings.IsActive { + if settings.IsEnabled { if settings.UniqueID == "" { settings.UniqueID = CreateUUID() } @@ -159,7 +159,7 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry // Send telemetry approval event approval := telemetry.Enabled - if !settings.IsActive { + if !settings.IsEnabled { approval = telemetry.Disabled } diff --git a/cmd/create_telemetry/create_telemetry_test.go b/cmd/create_telemetry/create_telemetry_test.go index d573d0042..f54bdcfb8 100644 --- a/cmd/create_telemetry/create_telemetry_test.go +++ b/cmd/create_telemetry/create_telemetry_test.go @@ -103,7 +103,7 @@ func TestLoadTelemetrySettings(t *testing.T) { }, want: want{ settings: settings.TelemetrySettings{ - IsActive: true, + IsEnabled: true, HasAnsweredPrompt: true, UserID: userId, UniqueID: uniqueId, @@ -126,7 +126,7 @@ func TestLoadTelemetrySettings(t *testing.T) { }, want: want{ settings: settings.TelemetrySettings{ - IsActive: false, + IsEnabled: false, HasAnsweredPrompt: true, }, telemetryEvents: []telemetry.Event{ @@ -146,7 +146,7 @@ func TestLoadTelemetrySettings(t *testing.T) { }, want: want{ settings: settings.TelemetrySettings{ - IsActive: true, + IsEnabled: true, HasAnsweredPrompt: true, UserID: userId, UniqueID: "other-id", diff --git a/cmd/telemetry.go b/cmd/telemetry.go index 89c52192d..da327cbbf 100644 --- a/cmd/telemetry.go +++ b/cmd/telemetry.go @@ -51,7 +51,7 @@ func setIsTelemetryActive(apiClient create_telemetry.TelemetryAPIClient, isActiv } settings.HasAnsweredPrompt = true - settings.IsActive = isActive + settings.IsEnabled = isActive if settings.UniqueID == "" { settings.UniqueID = create_telemetry.CreateUUID() diff --git a/cmd/telemetry_test.go b/cmd/telemetry_test.go index 764927de2..e8e09145a 100644 --- a/cmd/telemetry_test.go +++ b/cmd/telemetry_test.go @@ -45,7 +45,7 @@ func TestSetIsTelemetryActive(t *testing.T) { apiClient: telemetryTestAPIClient{}, isActive: true, settings: &settings.TelemetrySettings{ - IsActive: false, + IsEnabled: false, HasAnsweredPrompt: true, UniqueID: uniqueId, UserID: userId, @@ -53,7 +53,7 @@ func TestSetIsTelemetryActive(t *testing.T) { }, want: want{ settings: &settings.TelemetrySettings{ - IsActive: true, + IsEnabled: true, HasAnsweredPrompt: true, UniqueID: uniqueId, UserID: userId, @@ -69,7 +69,7 @@ func TestSetIsTelemetryActive(t *testing.T) { }, want: want{ settings: &settings.TelemetrySettings{ - IsActive: true, + IsEnabled: true, HasAnsweredPrompt: true, UniqueID: uniqueId, UserID: userId, @@ -82,7 +82,7 @@ func TestSetIsTelemetryActive(t *testing.T) { apiClient: telemetryTestAPIClient{}, isActive: false, settings: &settings.TelemetrySettings{ - IsActive: true, + IsEnabled: true, HasAnsweredPrompt: true, UniqueID: uniqueId, UserID: userId, @@ -90,7 +90,7 @@ func TestSetIsTelemetryActive(t *testing.T) { }, want: want{ settings: &settings.TelemetrySettings{ - IsActive: false, + IsEnabled: false, HasAnsweredPrompt: true, UniqueID: uniqueId, UserID: userId, @@ -106,7 +106,7 @@ func TestSetIsTelemetryActive(t *testing.T) { }, want: want{ settings: &settings.TelemetrySettings{ - IsActive: false, + IsEnabled: false, HasAnsweredPrompt: true, UniqueID: uniqueId, UserID: userId, diff --git a/cmd/version.go b/cmd/version.go index 8470333db..127d528c8 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -35,6 +35,7 @@ func newVersionCommand(config *settings.Config) *cobra.Command { opts.args = args }, Run: func(_ *cobra.Command, _ []string) { + fmt.Printf("telemetry.SegmentKey = %+v\n", telemetry.SegmentKey) fmt.Printf("%s+%s (%s)\n", version.Version, version.Commit, version.PackageManager()) }, } diff --git a/settings/settings.go b/settings/settings.go index b31797fe2..8e920e5f5 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -63,7 +63,7 @@ type UpdateCheck struct { // TelemetrySettings is used to represent telemetry related settings type TelemetrySettings struct { - IsActive bool `yaml:"is_active"` + IsEnabled bool `yaml:"is_enabled"` HasAnsweredPrompt bool `yaml:"has_answered_prompt"` UniqueID string `yaml:"unique_id"` UserID string `yaml:"user_id"` diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 906ef1003..29cf9832f 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -85,8 +85,8 @@ func (cli nullClient) Track(_ Event) error { return nil } // Used when telemetry is enabled type segmentClient struct { - cli analytics.Client - user User + analyticsClient analytics.Client + user User } func newSegmentClient(user User) Client { @@ -145,7 +145,7 @@ func (segment *segmentClient) Track(event Event) error { event.Properties["team_name"] = segment.user.TeamName } - return segment.cli.Enqueue(analytics.Track{ + return segment.analyticsClient.Enqueue(analytics.Track{ UserId: segment.user.UniqueID, Event: event.Object, Properties: event.Properties, @@ -153,7 +153,7 @@ func (segment *segmentClient) Track(event Event) error { } func (segment *segmentClient) Close() error { - return segment.cli.Close() + return segment.analyticsClient.Close() } // File telemetry From eaf3f6b4ce71e7733e7971852a73437c35dc7ebf Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Sun, 16 Jul 2023 10:23:01 +0200 Subject: [PATCH 26/36] moved telemetry in command context --- cmd/build.go | 10 ++--- cmd/completion.go | 8 ++-- cmd/config.go | 37 ++++++----------- cmd/config_test.go | 2 +- .../create_telemetry.go | 10 +++-- .../create_telemetry_test.go | 2 +- cmd/diagnostic.go | 12 +++--- cmd/diagnostic_test.go | 4 +- cmd/follow.go | 12 +++--- cmd/info/info.go | 18 ++++---- cmd/info/info_test.go | 41 +++++++++++-------- cmd/namespace.go | 10 ++--- cmd/namespace_test.go | 11 ++--- cmd/open.go | 12 +++--- cmd/orb.go | 8 ++-- cmd/orb_test.go | 2 +- cmd/policy/policy.go | 8 ++-- cmd/root.go | 28 ++++++++++--- cmd/root_test.go | 6 +-- cmd/runner/instance.go | 13 +++--- cmd/runner/resource_class.go | 8 ++-- cmd/runner/resource_class_test.go | 3 +- cmd/runner/runner.go | 8 +--- cmd/runner/telemetry_test.go | 8 +++- cmd/runner/token.go | 8 ++-- cmd/setup.go | 15 ++++--- cmd/setup_test.go | 2 +- cmd/telemetry.go | 7 ++-- cmd/telemetry_test.go | 18 ++------ cmd/update.go | 9 ++-- cmd/update_test.go | 5 ++- cmd/version.go | 22 +++++----- cmd/version_test.go | 39 ++++++++++++++++++ telemetry/context.go | 16 ++++++++ telemetry/utils.go | 31 ++++++++++++++ 35 files changed, 268 insertions(+), 185 deletions(-) rename cmd/{create_telemetry => }/create_telemetry.go (96%) rename cmd/{create_telemetry => }/create_telemetry_test.go (99%) create mode 100644 cmd/version_test.go create mode 100644 telemetry/context.go create mode 100644 telemetry/utils.go diff --git a/cmd/build.go b/cmd/build.go index 04ae56d43..df4d110c4 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/local" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -18,11 +17,12 @@ func newLocalExecuteCommand(config *settings.Config) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, _ []string) error { - telemetryClient := create_telemetry.CreateTelemetry(config) - defer telemetryClient.Close() - err := local.Execute(cmd.Flags(), config, args) - _ = telemetryClient.Track(telemetry.CreateLocalExecuteEvent(create_telemetry.GetCommandInformation(cmd, true))) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateLocalExecuteEvent(telemetry.GetCommandInformation(cmd, true))) + } return err }, diff --git a/cmd/completion.go b/cmd/completion.go index d2f1e2aab..edeaa58d4 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -3,7 +3,6 @@ package cmd import ( "os" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" @@ -14,9 +13,10 @@ func newCompletionCommand(config *settings.Config) *cobra.Command { Use: "completion", Short: "Generate shell completion scripts", PersistentPreRun: func(cmd *cobra.Command, _ []string) { - telemetryClient := create_telemetry.CreateTelemetry(config) - defer telemetryClient.Close() - _ = telemetryClient.Track(telemetry.CreateCompletionCommand(create_telemetry.GetCommandInformation(cmd, false))) + 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() diff --git a/cmd/config.go b/cmd/config.go index fddb791a3..56a321dc5 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,7 +8,6 @@ import ( "github.com/CircleCI-Public/circleci-config/labeling" "github.com/CircleCI-Public/circleci-config/labeling/codebase" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/config" "github.com/CircleCI-Public/circleci-cli/filetree" "github.com/CircleCI-Public/circleci-cli/proxy" @@ -30,33 +29,21 @@ 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 = create_telemetry.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(cmd *cobra.Command, args []string) error { - defer closeTelemetryClient() err := packConfig(args) - _ = telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err)) + } return err }, Args: cobra.ExactArgs(1), @@ -69,8 +56,6 @@ 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") @@ -89,7 +74,10 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { IgnoreDeprecatedImages: ignoreDeprecatedImages, VerboseOutput: verboseOutput, }) - _ = telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err)) + } return err }, @@ -111,8 +99,6 @@ 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") @@ -131,7 +117,10 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { PipelineParamsFilePath: pipelineParamsFilePath, VerboseOutput: verboseOutput, }) - _ = telemetryClient.Track(telemetry.CreateConfigEvent(create_telemetry.GetCommandInformation(cmd, true), err)) + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err)) + } if err != nil { return err } diff --git a/cmd/config_test.go b/cmd/config_test.go index 7e2e41e80..86c5e8f6d 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -37,8 +37,8 @@ var _ = Describe("Config", func() { "config", "pack", "--skip-update-check", filepath.Join("testdata", "hugo-pack", ".circleci"), - "--mock-telemetry", tempSettings.TelemetryDestPath, ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) }) AfterEach(func() { diff --git a/cmd/create_telemetry/create_telemetry.go b/cmd/create_telemetry.go similarity index 96% rename from cmd/create_telemetry/create_telemetry.go rename to cmd/create_telemetry.go index 6425df310..9ef4c3eb6 100644 --- a/cmd/create_telemetry/create_telemetry.go +++ b/cmd/create_telemetry.go @@ -1,4 +1,4 @@ -package create_telemetry +package cmd import ( "fmt" @@ -62,8 +62,12 @@ func (client nullTelemetryAPIClient) GetMyUserId() (string, error) { // 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) + mockTelemetry := config.MockTelemetry + if mockTelemetry == "" { + mockTelemetry = os.Getenv("MOCK_TELEMETRY") + } + if mockTelemetry != "" { + return telemetry.CreateFileTelemetry(mockTelemetry) } if config.IsTelemetryDisabled { diff --git a/cmd/create_telemetry/create_telemetry_test.go b/cmd/create_telemetry_test.go similarity index 99% rename from cmd/create_telemetry/create_telemetry_test.go rename to cmd/create_telemetry_test.go index f54bdcfb8..dfddc6150 100644 --- a/cmd/create_telemetry/create_telemetry_test.go +++ b/cmd/create_telemetry_test.go @@ -1,4 +1,4 @@ -package create_telemetry +package cmd import ( "path/filepath" diff --git a/cmd/diagnostic.go b/cmd/diagnostic.go index 44784c2ff..b8053d63f 100644 --- a/cmd/diagnostic.go +++ b/cmd/diagnostic.go @@ -6,7 +6,6 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" @@ -30,12 +29,13 @@ func newDiagnosticCommand(config *settings.Config) *cobra.Command { opts.args = args opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) }, - RunE: func(_ *cobra.Command, _ []string) error { - telemetryClient := create_telemetry.CreateTelemetry(config) - defer telemetryClient.Close() - + RunE: func(cmd *cobra.Command, _ []string) error { err := diagnostic(opts) - _ = telemetryClient.Track(telemetry.CreateDiagnosticEvent(err)) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateDiagnosticEvent(err)) + } return err }, diff --git a/cmd/diagnostic_test.go b/cmd/diagnostic_test.go index a298c9ab5..21071d3e3 100644 --- a/cmd/diagnostic_test.go +++ b/cmd/diagnostic_test.go @@ -85,8 +85,8 @@ var _ = Describe("Diagnostic", func() { command = commandWithHome(pathCLI, tempSettings.Home, "diagnostic", "--skip-update-check", - "--host", tempSettings.TestServer.URL(), - "--mock-telemetry", tempSettings.TelemetryDestPath) + "--host", tempSettings.TestServer.URL()) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) tempSettings.Config.Write([]byte(`token: mytoken`)) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) diff --git a/cmd/follow.go b/cmd/follow.go index 9eac8b942..9512423ee 100644 --- a/cmd/follow.go +++ b/cmd/follow.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/CircleCI-Public/circleci-cli/api" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/git" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -55,12 +54,13 @@ func followProjectCommand(config *settings.Config) *cobra.Command { followCommand := &cobra.Command{ Use: "follow", Short: "Attempt to follow the project for the current git repository.", - RunE: func(_ *cobra.Command, _ []string) error { - telemetryClient := create_telemetry.CreateTelemetry(config) - defer telemetryClient.Close() - + RunE: func(cmd *cobra.Command, _ []string) error { err := followProject(opts) - _ = telemetryClient.Track(telemetry.CreateFollowEvent(err)) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateFollowEvent(err)) + } return err }, diff --git a/cmd/info/info.go b/cmd/info/info.go index ade982b97..fce0bad59 100644 --- a/cmd/info/info.go +++ b/cmd/info/info.go @@ -2,7 +2,6 @@ package info import ( "github.com/CircleCI-Public/circleci-cli/api/info" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -13,9 +12,8 @@ import ( // infoOptions info command options type infoOptions struct { - cfg *settings.Config - validator validator.Validator - createTelemetry func() telemetry.Client + cfg *settings.Config + validator validator.Validator } // NewInfoCommand information cobra command creation @@ -25,9 +23,6 @@ func NewInfoCommand(config *settings.Config, preRunE validator.Validator) *cobra opts := infoOptions{ cfg: config, validator: preRunE, - createTelemetry: func() telemetry.Client { - return create_telemetry.CreateTelemetry(config) - }, } infoCommand := &cobra.Command{ Use: "info", @@ -47,11 +42,12 @@ func orgInfoCommand(client info.InfoClient, opts infoOptions) *cobra.Command { Long: `View your Organizations' names and ids.`, PreRunE: opts.validator, RunE: func(cmd *cobra.Command, _ []string) error { - telemetryClient := opts.createTelemetry() - defer telemetryClient.Close() - err := getOrgInformation(cmd, client) - _ = telemetryClient.Track(telemetry.CreateInfoEvent(create_telemetry.GetCommandInformation(cmd, true), err)) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateInfoEvent(telemetry.GetCommandInformation(cmd, true), err)) + } return err }, diff --git a/cmd/info/info_test.go b/cmd/info/info_test.go index 3ed51bdba..7b49c8784 100644 --- a/cmd/info/info_test.go +++ b/cmd/info/info_test.go @@ -2,14 +2,12 @@ package info import ( "bytes" - "encoding/json" + "context" "fmt" "net/http" "net/http/httptest" - "os" "testing" - "github.com/CircleCI-Public/circleci-cli/clitest" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -110,10 +108,19 @@ func TestFailedValidator(t *testing.T) { assert.Error(t, err, errorMessage) } -func TestTelemetry(t *testing.T) { - tempSettings := clitest.WithTempSettings() - defer tempSettings.Close() +type testTelemetryClient struct { + events []telemetry.Event +} + +func (cli *testTelemetryClient) Track(event telemetry.Event) error { + cli.events = append(cli.events, event) + return nil +} +func (cli *testTelemetryClient) Close() error { return nil } + +func TestTelemetry(t *testing.T) { + telemetryClient := testTelemetryClient{make([]telemetry.Event, 0)} // Test server var serverHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -124,25 +131,25 @@ func TestTelemetry(t *testing.T) { // Test command config := &settings.Config{ - Token: "testtoken", - HTTPClient: http.DefaultClient, - Host: server.URL, - MockTelemetry: tempSettings.TelemetryDestPath, + Token: "testtoken", + HTTPClient: http.DefaultClient, + Host: server.URL, } cmd := NewInfoCommand(config, nil) + cmd.SetArgs([]string{"org"}) + ctx := cmd.Context() + if ctx == nil { + ctx = context.Background() + } + cmd.SetContext(telemetry.NewContext(ctx, &telemetryClient)) // Execute err := cmd.Execute() - assert.NilError(t, err) - // We compare the - content, err := os.ReadFile(tempSettings.TelemetryDestPath) assert.NilError(t, err) - result := []telemetry.Event{} - err = json.Unmarshal(content, &result) - assert.NilError(t, err) - assert.DeepEqual(t, result, []telemetry.Event{ + // Read the telemetry events and compare them + assert.DeepEqual(t, telemetryClient.events, []telemetry.Event{ telemetry.CreateInfoEvent(telemetry.CommandInfo{ Name: "org", LocalArgs: map[string]string{"help": "false"}, diff --git a/cmd/namespace.go b/cmd/namespace.go index e9c0bf912..37cf6cbab 100644 --- a/cmd/namespace.go +++ b/cmd/namespace.go @@ -6,7 +6,6 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/prompt" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -70,9 +69,6 @@ Please note that at this time all namespaces created in the registry are world-r return validateToken(opts.cfg) }, RunE: func(cmd *cobra.Command, _ []string) error { - telemetryClient := create_telemetry.CreateTelemetry(config) - defer telemetryClient.Close() - if opts.integrationTesting { opts.tty = createNamespaceTestUI{ confirm: true, @@ -80,7 +76,11 @@ Please note that at this time all namespaces created in the registry are world-r } err := createNamespace(cmd, opts) - _ = telemetryClient.Track(telemetry.CreateNamespaceEvent(create_telemetry.GetCommandInformation(cmd, true))) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateNamespaceEvent(telemetry.GetCommandInformation(cmd, true))) + } return err }, diff --git a/cmd/namespace_test.go b/cmd/namespace_test.go index 06fff16e1..216817c96 100644 --- a/cmd/namespace_test.go +++ b/cmd/namespace_test.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" "os/exec" - "path/filepath" "github.com/CircleCI-Public/circleci-cli/clitest" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -16,10 +15,9 @@ import ( var _ = Describe("Namespace integration tests", func() { var ( - tempSettings *clitest.TempSettings - token string = "testtoken" - command *exec.Cmd - telemetryDestFilePath string + tempSettings *clitest.TempSettings + token string = "testtoken" + command *exec.Cmd ) BeforeEach(func() { @@ -32,7 +30,6 @@ var _ = Describe("Namespace integration tests", func() { Describe("telemetry", func() { It("sends expected event", func() { - telemetryDestFilePath = filepath.Join(tempSettings.Home, "telemetry-content") command = exec.Command(pathCLI, "namespace", "create", "--skip-update-check", @@ -41,8 +38,8 @@ var _ = Describe("Namespace integration tests", func() { "--integration-testing", "foo-ns", "--org-id", `"bb604b45-b6b0-4b81-ad80-796f15eddf87"`, - "--mock-telemetry", telemetryDestFilePath, ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusOK) diff --git a/cmd/open.go b/cmd/open.go index e1cbc4adf..2d9183550 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -5,7 +5,6 @@ import ( "net/url" "strings" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/git" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -46,12 +45,13 @@ 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 { - telemetryClient := create_telemetry.CreateTelemetry(config) - defer telemetryClient.Close() - + RunE: func(cmd *cobra.Command, _ []string) error { err := openProjectInBrowser() - _ = telemetryClient.Track(telemetry.CreateOpenEvent(err)) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateOpenEvent(err)) + } return err }, diff --git a/cmd/orb.go b/cmd/orb.go index f8443706b..d7fdd5394 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -20,7 +20,6 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/collaborators" "github.com/CircleCI-Public/circleci-cli/api/graphql" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/filetree" "github.com/CircleCI-Public/circleci-cli/process" "github.com/CircleCI-Public/circleci-cli/prompt" @@ -390,9 +389,10 @@ Please note that at this time all orbs created in the registry are world-readabl opts.args = args opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) - telemetryClient := create_telemetry.CreateTelemetry(config) - defer telemetryClient.Close() - _ = telemetryClient.Track(telemetry.CreateOrbEvent(create_telemetry.GetCommandInformation(cmd, true))) + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateOrbEvent(telemetry.GetCommandInformation(cmd, true))) + } // PersistentPreRunE overwrites the inherited persistent hook from rootCmd // So we explicitly call it here to retain that behavior. diff --git a/cmd/orb_test.go b/cmd/orb_test.go index cd0741144..a33ef59ff 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -39,8 +39,8 @@ var _ = Describe("Orb telemetry", func() { "--skip-update-check", "--token", "token", "--host", tempSettings.TestServer.URL(), - "--mock-telemetry", tempSettings.TelemetryDestPath, ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) }) AfterEach(func() { diff --git a/cmd/policy/policy.go b/cmd/policy/policy.go index be13a0ae4..bd4a2260b 100644 --- a/cmd/policy/policy.go +++ b/cmd/policy/policy.go @@ -24,7 +24,6 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/policy" "github.com/CircleCI-Public/circleci-cli/api/rest" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/config" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -37,9 +36,10 @@ func NewCommand(globalConfig *settings.Config, preRunE validator.Validator) *cob cmd := &cobra.Command{ Use: "policy", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - telemetryClient := create_telemetry.CreateTelemetry(globalConfig) - defer telemetryClient.Close() - _ = telemetryClient.Track(telemetry.CreatePolicyEvent(create_telemetry.GetCommandInformation(cmd, true))) + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreatePolicyEvent(telemetry.GetCommandInformation(cmd, true))) + } if preRunE != nil { return preRunE(cmd, args) diff --git a/cmd/root.go b/cmd/root.go index d3cfd3d5d..bff225e82 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "log" "os" @@ -14,6 +15,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/data" "github.com/CircleCI-Public/circleci-cli/md_docs" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/version" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" @@ -36,14 +38,28 @@ var rootOptions *settings.Config // rootTokenFromFlag stores the value passed in through the flag --token var rootTokenFromFlag string -// Execute adds all child commands to rootCmd and -// sets flags appropriately. This function is called -// by main.main(). It only needs to happen once to -// the rootCmd. +// Execute adds all child commands to rootCmd, sets the flags appropriately +// and put the telemetry client in the command context. This function is +// called by main.main(). It only needs to happen once to the rootCmd func Execute() { header.SetCommandStr(CommandStr()) command := MakeCommands() - if err := command.Execute(); err != nil { + + // If we have no context, we can't add the telemetry to the context + if command.Context() == nil { + command.SetContext(context.Background()) + } + + telemetryClient := CreateTelemetry(rootOptions) + // We defer to close the telemetry in case of panic + defer telemetryClient.Close() + command.SetContext(telemetry.NewContext(command.Context(), telemetryClient)) + + err := command.Execute() + // We close here because defer is not called when `os.Exit` is called + telemetryClient.Close() + + if err != nil { os.Exit(-1) } } @@ -123,7 +139,7 @@ func MakeCommands() *cobra.Command { Use: "circleci", Long: longHelp, Short: rootHelpShort(rootOptions), - PersistentPreRunE: func(_ *cobra.Command, _ []string) error { + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { return rootCmdPreRun(rootOptions) }, } diff --git a/cmd/root_test.go b/cmd/root_test.go index 515a9294e..d05a85419 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -52,8 +52,7 @@ var _ = Describe("Root", func() { Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out).Should(gbytes.Say("update This command is unavailable on your platform")) - + Eventually(session.Out).Should(gbytes.Say("update\\s+This command is unavailable on your platform")) Eventually(session).Should(gexec.Exit(0)) }) @@ -92,8 +91,7 @@ var _ = Describe("Root", func() { Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out).Should(gbytes.Say("update Update the tool to the latest version")) - + Eventually(session.Out).Should(gbytes.Say("update\\s+Update the tool to the latest version")) Eventually(session).Should(gexec.Exit(0)) }) }) diff --git a/cmd/runner/instance.go b/cmd/runner/instance.go index 9a2f869b5..b9ce6bba1 100644 --- a/cmd/runner/instance.go +++ b/cmd/runner/instance.go @@ -8,7 +8,6 @@ import ( "github.com/spf13/cobra" "github.com/CircleCI-Public/circleci-cli/api/runner" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/telemetry" ) @@ -27,14 +26,14 @@ func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra Aliases: []string{"ls"}, Args: cobra.ExactArgs(1), PreRunE: preRunE, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { var err error - telemetryClient := o.createTelemetry() - defer (func() { - _ = telemetryClient.Track(telemetry.CreateRunnerInstanceEvent(create_telemetry.GetCommandInformation(cmd, true), err)) - _ = telemetryClient.Close() - })() + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + // We defer the call to be sure the `err` has been filled + defer telemetryClient.Track(telemetry.CreateRunnerInstanceEvent(telemetry.GetCommandInformation(cmd, true), err)) + } runners, err := o.r.GetRunnerInstances(args[0]) if err != nil { diff --git a/cmd/runner/resource_class.go b/cmd/runner/resource_class.go index 4cfad2d12..4b68b37ae 100644 --- a/cmd/runner/resource_class.go +++ b/cmd/runner/resource_class.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" "github.com/CircleCI-Public/circleci-cli/api/runner" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/telemetry" ) @@ -17,9 +16,10 @@ func newResourceClassCommand(o *runnerOpts, preRunE validator.Validator) *cobra. Use: "resource-class", Short: "Operate on runner resource-classes", PersistentPreRun: func(cmd *cobra.Command, _ []string) { - telemetryClient := o.createTelemetry() - defer telemetryClient.Close() - _ = telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(create_telemetry.GetCommandInformation(cmd, true))) + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(telemetry.GetCommandInformation(cmd, true))) + } }, } diff --git a/cmd/runner/resource_class_test.go b/cmd/runner/resource_class_test.go index cf1cd3390..df9cbbf27 100644 --- a/cmd/runner/resource_class_test.go +++ b/cmd/runner/resource_class_test.go @@ -12,12 +12,11 @@ import ( "gotest.tools/v3/assert/cmp" "github.com/CircleCI-Public/circleci-cli/api/runner" - "github.com/CircleCI-Public/circleci-cli/telemetry" ) func Test_ResourceClass(t *testing.T) { runner := runnerMock{} - cmd := newResourceClassCommand(&runnerOpts{r: &runner, createTelemetry: telemetry.CreateNullClient}, nil) + cmd := newResourceClassCommand(&runnerOpts{r: &runner}, nil) stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) cmd.SetOut(stdout) diff --git a/cmd/runner/runner.go b/cmd/runner/runner.go index 1eb3d0d3d..72424c1b6 100644 --- a/cmd/runner/runner.go +++ b/cmd/runner/runner.go @@ -7,15 +7,12 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/rest" "github.com/CircleCI-Public/circleci-cli/api/runner" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/settings" - "github.com/CircleCI-Public/circleci-cli/telemetry" ) type runnerOpts struct { - r running - createTelemetry func() telemetry.Client + r running } func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Command { @@ -31,9 +28,6 @@ func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Com host = config.Host } opts.r = runner.New(rest.NewFromConfig(host, config)) - opts.createTelemetry = func() telemetry.Client { - return create_telemetry.CreateTelemetry(config) - } }, } diff --git a/cmd/runner/telemetry_test.go b/cmd/runner/telemetry_test.go index 256b27076..dcd12874c 100644 --- a/cmd/runner/telemetry_test.go +++ b/cmd/runner/telemetry_test.go @@ -2,6 +2,7 @@ package runner import ( "bytes" + "context" "testing" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -23,11 +24,16 @@ func Test_RunnerTelemetry(t *testing.T) { t.Run("resource-class", func(t *testing.T) { telemetryClient := &testTelemetryClient{make([]telemetry.Event, 0)} runner := runnerMock{} - cmd := newResourceClassCommand(&runnerOpts{r: &runner, createTelemetry: func() telemetry.Client { return telemetryClient }}, nil) + cmd := newResourceClassCommand(&runnerOpts{r: &runner}, nil) stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) cmd.SetOut(stdout) cmd.SetErr(stderr) + ctx := cmd.Context() + if ctx == nil { + ctx = context.Background() + } + cmd.SetContext(telemetry.NewContext(ctx, telemetryClient)) defer runner.reset() defer stdout.Reset() diff --git a/cmd/runner/token.go b/cmd/runner/token.go index 2a249c5a4..60edcbf6e 100644 --- a/cmd/runner/token.go +++ b/cmd/runner/token.go @@ -3,7 +3,6 @@ package runner import ( "time" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/olekukonko/tablewriter" @@ -15,9 +14,10 @@ func newTokenCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command Use: "token", Short: "Operate on runner tokens", PersistentPreRun: func(cmd *cobra.Command, _ []string) { - telemetryClient := o.createTelemetry() - defer telemetryClient.Close() - _ = telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(create_telemetry.GetCommandInformation(cmd, true))) + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(telemetry.GetCommandInformation(cmd, true))) + } }, } diff --git a/cmd/setup.go b/cmd/setup.go index 2c8e174a5..88db2d8ed 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -5,7 +5,6 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/prompt" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" @@ -125,7 +124,13 @@ func newSetupCommand(config *settings.Config) *cobra.Command { opts.args = args opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) }, - RunE: func(_ *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + // We defer the call to make sure the `opts.cfg.Host` has been filled + defer telemetryClient.Track(telemetry.CreateSetupEvent(opts.cfg.Host != defaultHost)) + } + if opts.integrationTesting { opts.tty = setupTestUI{ host: "boondoggle", @@ -191,12 +196,6 @@ func setup(opts setupOptions) error { setupDiagnosticCheck(opts) } - telemetryClient := create_telemetry.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 } diff --git a/cmd/setup_test.go b/cmd/setup_test.go index c7c38e10e..0367bc5e1 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -28,8 +28,8 @@ var _ = Describe("Setup telemetry", func() { "setup", "--integration-testing", "--skip-update-check", - "--mock-telemetry", tempSettings.TelemetryDestPath, ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) }) AfterEach(func() { diff --git a/cmd/telemetry.go b/cmd/telemetry.go index da327cbbf..79ad04035 100644 --- a/cmd/telemetry.go +++ b/cmd/telemetry.go @@ -3,14 +3,13 @@ package cmd import ( "os" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/pkg/errors" "github.com/spf13/cobra" ) func newTelemetryCommand(config *settings.Config) *cobra.Command { - apiClient := create_telemetry.CreateAPIClient(config) + apiClient := CreateAPIClient(config) telemetryEnable := &cobra.Command{ Use: "enable", @@ -44,7 +43,7 @@ Note: If you have not configured your telemetry preferences and call the CLI wit return telemetryCommand } -func setIsTelemetryActive(apiClient create_telemetry.TelemetryAPIClient, isActive bool) error { +func setIsTelemetryActive(apiClient TelemetryAPIClient, isActive bool) error { settings := settings.TelemetrySettings{} if err := settings.Load(); err != nil && !os.IsNotExist(err) { return errors.Wrap(err, "Loading telemetry configuration") @@ -54,7 +53,7 @@ func setIsTelemetryActive(apiClient create_telemetry.TelemetryAPIClient, isActiv settings.IsEnabled = isActive if settings.UniqueID == "" { - settings.UniqueID = create_telemetry.CreateUUID() + settings.UniqueID = CreateUUID() } if settings.UserID == "" { diff --git a/cmd/telemetry_test.go b/cmd/telemetry_test.go index e8e09145a..468e1ce9d 100644 --- a/cmd/telemetry_test.go +++ b/cmd/telemetry_test.go @@ -4,24 +4,14 @@ import ( "path/filepath" "testing" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/spf13/afero" "gotest.tools/v3/assert" ) -type telemetryTestAPIClient struct { - id string - err error -} - -func (me telemetryTestAPIClient) GetMyUserId() (string, error) { - return me.id, me.err -} - func TestSetIsTelemetryActive(t *testing.T) { type args struct { - apiClient create_telemetry.TelemetryAPIClient + apiClient TelemetryAPIClient isActive bool settings *settings.TelemetrySettings } @@ -116,9 +106,9 @@ func TestSetIsTelemetryActive(t *testing.T) { } // Mock create UUID - oldUUIDCreate := create_telemetry.CreateUUID - create_telemetry.CreateUUID = func() string { return uniqueId } - defer (func() { create_telemetry.CreateUUID = oldUUIDCreate })() + oldUUIDCreate := CreateUUID + CreateUUID = func() string { return uniqueId } + defer (func() { CreateUUID = oldUUIDCreate })() for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/update.go b/cmd/update.go index a326fbbb2..5ef2bcc44 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -4,7 +4,6 @@ import ( "fmt" "time" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/update" @@ -32,9 +31,11 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { Short: "Update the tool to the latest version", PersistentPreRun: func(cmd *cobra.Command, _ []string) { opts.cfg.SkipUpdateCheck = true - telemetryClient := create_telemetry.CreateTelemetry(config) - defer telemetryClient.Close() - _ = telemetryClient.Track(telemetry.CreateUpdateEvent(create_telemetry.GetCommandInformation(cmd, cmd.Name() != "update"))) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateUpdateEvent(telemetry.GetCommandInformation(cmd, cmd.Name() != "update"))) + } }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args diff --git a/cmd/update_test.go b/cmd/update_test.go index 97fec68bc..6ed7aa5dd 100644 --- a/cmd/update_test.go +++ b/cmd/update_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "fmt" "net/http" "os/exec" "path/filepath" @@ -79,8 +80,8 @@ var _ = Describe("Update", func() { command = exec.Command(updateCLI, "update", "--github-api", tempSettings.TestServer.URL(), - "--mock-telemetry", tempSettings.TelemetryDestPath, ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) assetBytes := golden.Get(GinkgoT(), filepath.FromSlash("update/foo.zip")) assetResponse := string(assetBytes) @@ -116,8 +117,8 @@ var _ = Describe("Update", func() { "update", "check", "--github-api", tempSettings.TestServer.URL(), - "--mock-telemetry", tempSettings.TelemetryDestPath, ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) diff --git a/cmd/version.go b/cmd/version.go index 127d528c8..6ce3d8ad8 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" - "github.com/CircleCI-Public/circleci-cli/cmd/create_telemetry" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/version" @@ -19,24 +18,27 @@ 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 = create_telemetry.CreateTelemetry(config) - defer telemetryClient.Close() - _ = telemetryClient.Track(telemetry.CreateVersionEvent(version.Version)) - opts.cfg.SkipUpdateCheck = true }, - PreRun: func(cmd *cobra.Command, args []string) { + PreRun: func(_ *cobra.Command, args []string) { opts.args = args }, - Run: func(_ *cobra.Command, _ []string) { - fmt.Printf("telemetry.SegmentKey = %+v\n", telemetry.SegmentKey) - fmt.Printf("%s+%s (%s)\n", version.Version, version.Commit, version.PackageManager()) + Run: func(cmd *cobra.Command, _ []string) { + version := fmt.Sprintf("%s+%s (%s)", version.Version, version.Commit, version.PackageManager()) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + fmt.Printf("telemetryClient = %+v\n", telemetryClient) + fmt.Printf("ok = %+v\n", ok) + if ok { + _ = telemetryClient.Track(telemetry.CreateVersionEvent(version)) + } + + fmt.Printf("%s\n", version) }, } } diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 000000000..e64cf051e --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,39 @@ +package cmd_test + +import ( + "fmt" + "os/exec" + + "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" +) + +var _ = Describe("Version telemetry", func() { + var ( + command *exec.Cmd + tempSettings *clitest.TempSettings + ) + + BeforeEach(func() { + tempSettings = clitest.WithTempSettings() + command = commandWithHome(pathCLI, tempSettings.Home, "version") + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + }) + + AfterEach(func() { + tempSettings.Close() + }) + + It("should send a 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.CreateVersionEvent("0.0.0-dev+dirty-local-tree (source)"), + }) + }) +}) diff --git a/telemetry/context.go b/telemetry/context.go new file mode 100644 index 000000000..4e245f101 --- /dev/null +++ b/telemetry/context.go @@ -0,0 +1,16 @@ +package telemetry + +import "context" + +type contextKey string + +const telemetryClientContextKey contextKey = "telemetryClientContextKey" + +func NewContext(ctx context.Context, client Client) context.Context { + return context.WithValue(ctx, telemetryClientContextKey, client) +} + +func FromContext(ctx context.Context) (Client, bool) { + client, ok := ctx.Value(telemetryClientContextKey).(Client) + return client, ok +} diff --git a/telemetry/utils.go b/telemetry/utils.go new file mode 100644 index 000000000..2201887ad --- /dev/null +++ b/telemetry/utils.go @@ -0,0 +1,31 @@ +package telemetry + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// Utility function used when creating telemetry events. +// It takes a cobra Command and creates a telemetry.CommandInfo of it +// If getParent is true, puts both the command's args in `LocalArgs` and the parent's args +// Else only put the command's args +// Note: child flags overwrite parent flags with same name +func GetCommandInformation(cmd *cobra.Command, getParent bool) CommandInfo { + localArgs := map[string]string{} + + parent := cmd.Parent() + if getParent && parent != nil { + parent.LocalFlags().VisitAll(func(flag *pflag.Flag) { + localArgs[flag.Name] = flag.Value.String() + }) + } + + cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) { + localArgs[flag.Name] = flag.Value.String() + }) + + return CommandInfo{ + Name: cmd.Name(), + LocalArgs: localArgs, + } +} From f6d26cc03d6a10af1055560404cbf4c092bee086 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 18 Jul 2023 10:35:50 +0200 Subject: [PATCH 27/36] last changes --- cmd/root.go | 21 +++++++-------------- cmd/runner/instance.go | 4 +++- cmd/setup.go | 2 +- cmd/setup_test.go | 2 +- cmd/version.go | 2 -- main.go | 6 +++++- 6 files changed, 17 insertions(+), 20 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index bff225e82..309985c32 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -41,27 +41,20 @@ var rootTokenFromFlag string // Execute adds all child commands to rootCmd, sets the flags appropriately // and put the telemetry client in the command context. This function is // called by main.main(). It only needs to happen once to the rootCmd -func Execute() { +func Execute() error { header.SetCommandStr(CommandStr()) command := MakeCommands() - // If we have no context, we can't add the telemetry to the context - if command.Context() == nil { - command.SetContext(context.Background()) - } - telemetryClient := CreateTelemetry(rootOptions) - // We defer to close the telemetry in case of panic defer telemetryClient.Close() - command.SetContext(telemetry.NewContext(command.Context(), telemetryClient)) - - err := command.Execute() - // We close here because defer is not called when `os.Exit` is called - telemetryClient.Close() - if err != nil { - os.Exit(-1) + cmdContext := command.Context() + if cmdContext == nil { + cmdContext = context.Background() } + command.SetContext(telemetry.NewContext(cmdContext, telemetryClient)) + + return command.Execute() } // Returns a string (e.g. "circleci context list") indicating what diff --git a/cmd/runner/instance.go b/cmd/runner/instance.go index b9ce6bba1..a4da71338 100644 --- a/cmd/runner/instance.go +++ b/cmd/runner/instance.go @@ -32,7 +32,9 @@ func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra telemetryClient, ok := telemetry.FromContext(cmd.Context()) if ok { // We defer the call to be sure the `err` has been filled - defer telemetryClient.Track(telemetry.CreateRunnerInstanceEvent(telemetry.GetCommandInformation(cmd, true), err)) + defer (func() { + _ = telemetryClient.Track(telemetry.CreateRunnerInstanceEvent(telemetry.GetCommandInformation(cmd, true), err)) + })() } runners, err := o.r.GetRunnerInstances(args[0]) diff --git a/cmd/setup.go b/cmd/setup.go index 88db2d8ed..efc30288a 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -128,7 +128,7 @@ func newSetupCommand(config *settings.Config) *cobra.Command { telemetryClient, ok := telemetry.FromContext(cmd.Context()) if ok { // We defer the call to make sure the `opts.cfg.Host` has been filled - defer telemetryClient.Track(telemetry.CreateSetupEvent(opts.cfg.Host != defaultHost)) + defer (func() { _ = telemetryClient.Track(telemetry.CreateSetupEvent(opts.cfg.Host != defaultHost)) })() } if opts.integrationTesting { diff --git a/cmd/setup_test.go b/cmd/setup_test.go index 0367bc5e1..8ae09473c 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -42,7 +42,7 @@ var _ = Describe("Setup telemetry", func() { Eventually(session).Should(gexec.Exit(0)) clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ - telemetry.CreateSetupEvent(false), + telemetry.CreateSetupEvent(true), }) }) }) diff --git a/cmd/version.go b/cmd/version.go index 6ce3d8ad8..e2cd566ce 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -32,8 +32,6 @@ func newVersionCommand(config *settings.Config) *cobra.Command { version := fmt.Sprintf("%s+%s (%s)", version.Version, version.Commit, version.PackageManager()) telemetryClient, ok := telemetry.FromContext(cmd.Context()) - fmt.Printf("telemetryClient = %+v\n", telemetryClient) - fmt.Printf("ok = %+v\n", ok) if ok { _ = telemetryClient.Track(telemetry.CreateVersionEvent(version)) } diff --git a/main.go b/main.go index 8948a918d..9a733aa63 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,14 @@ package main import ( + "os" + "github.com/CircleCI-Public/circleci-cli/cmd" ) func main() { // See cmd/root.go for Execute() - cmd.Execute() + if err := cmd.Execute(); err != nil { + os.Exit(-1) + } } From c2680d8617b390549cf538c2b1ff97d55640e17e Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Thu, 20 Jul 2023 10:11:36 +0200 Subject: [PATCH 28/36] minor changes on events and build --- Makefile | 2 +- telemetry/events.go | 26 ++++++++++++++++++-------- telemetry/telemetry.go | 5 +++-- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 604d50148..31b518b01 100644 --- a/Makefile +++ b/Makefile @@ -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=' -X 'github.com/CircleCI-Public/circleci-cli/telemetry.SegmentKey="`echo ${SEGMENT_KEY}`"'" build-all: build/linux/amd64/circleci build/darwin/amd64/circleci diff --git a/telemetry/events.go b/telemetry/events.go index 5e8362171..2985364e6 100644 --- a/telemetry/events.go +++ b/telemetry/events.go @@ -17,6 +17,7 @@ func createEventFromCommandInfo(name string, cmdInfo CommandInfo) Event { for key, value := range cmdInfo.LocalArgs { properties[fmt.Sprintf("cmd.flag.%s", key)] = value } + properties["has_been_executed"] = false return Event{ Object: fmt.Sprintf("cli-%s", name), @@ -26,19 +27,23 @@ func createEventFromCommandInfo(name string, cmdInfo CommandInfo) Event { } func errorToProperties(err error) map[string]interface{} { - if err == nil { - return nil + properties := map[string]interface{}{ + "has_been_executed": true, } - return map[string]interface{}{ - "error": err.Error(), + + if err != nil { + properties["error"] = err.Error() } + return properties } func CreateSetupEvent(isServerCustomer bool) Event { return Event{ Object: "cli-setup", + Action: "setup", Properties: map[string]interface{}{ "is_server_customer": isServerCustomer, + "has_been_executed": true, }, } } @@ -46,8 +51,10 @@ func CreateSetupEvent(isServerCustomer bool) Event { func CreateVersionEvent(version string) Event { return Event{ Object: "cli-version", + Action: "version", Properties: map[string]interface{}{ - "version": version, + "version": version, + "has_been_executed": true, }, } } @@ -58,18 +65,18 @@ func CreateUpdateEvent(cmdInfo CommandInfo) Event { func CreateDiagnosticEvent(err error) Event { return Event{ - Object: "cli-diagnostic", Properties: errorToProperties(err), + Object: "cli-diagnostic", Action: "diagnostic", Properties: errorToProperties(err), } } func CreateFollowEvent(err error) Event { return Event{ - Object: "cli-follow", Properties: errorToProperties(err), + Object: "cli-follow", Action: "follow", Properties: errorToProperties(err), } } func CreateOpenEvent(err error) Event { - return Event{Object: "cli-open", Properties: errorToProperties(err)} + return Event{Object: "cli-open", Action: "open", Properties: errorToProperties(err)} } func CreateCompletionCommand(cmdInfo CommandInfo) Event { @@ -80,6 +87,7 @@ func CreateConfigEvent(cmdInfo CommandInfo, err error) Event { event := createEventFromCommandInfo("config", cmdInfo) if err != nil { event.Properties["error"] = err.Error() + event.Properties["has_been_executed"] = true } return event } @@ -104,6 +112,7 @@ func CreateRunnerInstanceEvent(cmdInfo CommandInfo, err error) Event { event := createEventFromCommandInfo("runner-instance", cmdInfo) if err != nil { event.Properties["error"] = err.Error() + event.Properties["has_been_executed"] = true } return event } @@ -120,6 +129,7 @@ func CreateInfoEvent(cmdInfo CommandInfo, err error) Event { event := createEventFromCommandInfo("info", cmdInfo) if err != nil { event.Properties["error"] = err.Error() + event.Properties["has_been_executed"] = true } return event } diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 29cf9832f..712fcd614 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -61,6 +61,7 @@ func CreateClient(user User, enabled bool) Client { // Sends the user's approval event func SendTelemetryApproval(user User, approval Approval) error { client := CreateActiveTelemetry(user) + defer client.Close() return client.Track(Event{ Object: "cli-telemetry", @@ -124,11 +125,11 @@ func (segment *segmentClient) Track(event Event) error { } if segment.user.UniqueID != "" { - event.Properties["UUID"] = segment.user.UniqueID + event.Properties["anonymous_id"] = segment.user.UniqueID } if segment.user.UserID != "" { - event.Properties["user_id"] = segment.user.UserID + event.Properties["cci_user_id"] = segment.user.UserID } event.Properties["is_self_hosted"] = segment.user.IsSelfHosted From c6a38446cbcc8525b82bbcd861cdd5d13fe1952b Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Thu, 20 Jul 2023 16:29:44 +0200 Subject: [PATCH 29/36] Put segment key in the code Moved the key inside the code because it was not possible not to add it to the Homebrew formula so we decided to write it in the code directly --- Makefile | 2 +- Taskfile.yml | 2 +- cmd/create_telemetry.go | 2 +- cmd/root.go | 1 - telemetry/telemetry.go | 4 ++-- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 31b518b01..f857e87a5 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ GOOS=$(shell go env GOOS) GOARCH=$(shell go env GOARCH) build: always - go build -o build/$(GOOS)/$(GOARCH)/circleci -ldflags="-X 'github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=' -X 'github.com/CircleCI-Public/circleci-cli/telemetry.SegmentKey="`echo ${SEGMENT_KEY}`"'" + go build -o build/$(GOOS)/$(GOARCH)/circleci -ldflags='-X github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=' build-all: build/linux/amd64/circleci build/darwin/amd64/circleci diff --git a/Taskfile.yml b/Taskfile.yml index 2cbce4aae..54153650e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -49,7 +49,7 @@ tasks: cmds: # 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=' -X 'github.com/CircleCI-Public/circleci-cli/telemetry.SegmentKey="`echo ${SEGMENT_KEY}`"'" . + - go build -v -o build/$(go env GOOS)/$(go env GOARCH)/circleci -ldflags='-X github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=' . build-linux: desc: Build main diff --git a/cmd/create_telemetry.go b/cmd/create_telemetry.go index 9ef4c3eb6..11272e6c2 100644 --- a/cmd/create_telemetry.go +++ b/cmd/create_telemetry.go @@ -70,7 +70,7 @@ func CreateTelemetry(config *settings.Config) telemetry.Client { return telemetry.CreateFileTelemetry(mockTelemetry) } - if config.IsTelemetryDisabled { + if config.IsTelemetryDisabled || len(os.Getenv("CIRCLECI_CLI_TELEMETRY_OPTOUT")) != 0 { return telemetry.CreateClient(telemetry.User{}, false) } diff --git a/cmd/root.go b/cmd/root.go index 309985c32..05e028a99 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -194,7 +194,6 @@ func MakeCommands() *cobra.Command { flags.StringVar(&rootOptions.Endpoint, "endpoint", rootOptions.Endpoint, "URI to your CircleCI GraphQL API endpoint") 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", "mock-telemetry"} diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 712fcd614..74a959be3 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -14,7 +14,7 @@ var ( CreateActiveTelemetry = newSegmentClient SegmentEndpoint = "http://localhost" - SegmentKey = "" + segmentKey = "AbgkrgN4cbRhAVEwlzMkHbwvrXnxHh35" ) type Approval string @@ -91,7 +91,7 @@ type segmentClient struct { } func newSegmentClient(user User) Client { - cli, err := analytics.NewWithConfig(SegmentKey, analytics.Config{ + cli, err := analytics.NewWithConfig(segmentKey, analytics.Config{ Endpoint: SegmentEndpoint, }) From 299a28575aa2b26d11636bec8977b4f2e9620690 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Mon, 24 Jul 2023 09:31:08 +0200 Subject: [PATCH 30/36] fix tests --- Makefile | 2 +- Taskfile.yml | 2 +- telemetry/telemetry.go | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f857e87a5..a51a58c4a 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ GOOS=$(shell go env GOOS) GOARCH=$(shell go env GOARCH) build: always - go build -o build/$(GOOS)/$(GOARCH)/circleci -ldflags='-X github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=' + 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 diff --git a/Taskfile.yml b/Taskfile.yml index 54153650e..3cb8bfffc 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -49,7 +49,7 @@ tasks: cmds: # 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=' . + - 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 diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 74a959be3..5ffe0f5c5 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -85,6 +85,12 @@ func (cli nullClient) Track(_ Event) error { return nil } // Segment client // Used when telemetry is enabled +// Nil segment logger +type nilSegmentEmptyLogger struct{} + +func (nilSegmentEmptyLogger) Logf(format string, args ...interface{}) {} +func (nilSegmentEmptyLogger) Errorf(format string, args ...interface{}) {} + type segmentClient struct { analyticsClient analytics.Client user User @@ -93,6 +99,7 @@ type segmentClient struct { func newSegmentClient(user User) Client { cli, err := analytics.NewWithConfig(segmentKey, analytics.Config{ Endpoint: SegmentEndpoint, + Logger: nilSegmentEmptyLogger{}, }) if err != nil { From 092470d4b26566f48a9e61ccc8ace6fee8badf78 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Mon, 24 Jul 2023 15:53:47 +0200 Subject: [PATCH 31/36] fix telemetry key --- cmd/create_telemetry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/create_telemetry.go b/cmd/create_telemetry.go index 11272e6c2..69bafa84c 100644 --- a/cmd/create_telemetry.go +++ b/cmd/create_telemetry.go @@ -84,7 +84,7 @@ func CreateTelemetry(config *settings.Config) telemetry.Client { telemetrySettings := settings.TelemetrySettings{} user := telemetry.User{ - IsSelfHosted: config.Host == "https://circleci.com", + IsSelfHosted: config.Host != defaultHost, OS: runtime.GOOS, Version: version.Version, TeamName: "devex", From d53a66c8c41b1ac6a93272fe89deab909e9a8c43 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Mon, 24 Jul 2023 16:35:53 +0200 Subject: [PATCH 32/36] fix test for windows --- cmd/cmd_suite_test.go | 3 +++ telemetry/telemetry.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/cmd_suite_test.go b/cmd/cmd_suite_test.go index 5f126d986..5e54304ca 100644 --- a/cmd/cmd_suite_test.go +++ b/cmd/cmd_suite_test.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "testing" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -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()) diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 5ffe0f5c5..bb16bee0b 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -13,7 +13,7 @@ var ( // Overwrite this function for tests CreateActiveTelemetry = newSegmentClient - SegmentEndpoint = "http://localhost" + SegmentEndpoint = "http://localhost:33457" segmentKey = "AbgkrgN4cbRhAVEwlzMkHbwvrXnxHh35" ) From a1491d10919b38ec911c5a0eed3b045852e0b0a3 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 25 Jul 2023 09:47:28 +0200 Subject: [PATCH 33/36] Add telemetry information in docs --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f0be8430..c8fd82bc1 100644 --- a/README.md +++ b/README.md @@ -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 |
    • `circleci config validate`
    • `circleci config process`
    • `circleci local execute`
    • | The config validation has been moved from the GraphQL API to a specific API endpoint |
      • **Server v4.0.5, v4.1.3, v4.2.0 and above**: Commands use the new specific endpoint
      • **Previous version**: Commands use the GraphQL API
      | -| Orb compilation and validation of orb using private orbs |
      • `circleci orb process`
      • `circleci orb validate`
      | 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 |
      • **Server v4.2.0 and above**: The field is accessible so you can use the parameter
      • **Previous versions**: The field does not exist making the functionality unavailable
      | \ No newline at end of file +| Orb compilation and validation of orb using private orbs |
      • `circleci orb process`
      • `circleci orb validate`
      | 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 |
      • **Server v4.2.0 and above**: The field is accessible so you can use the parameter
      • **Previous versions**: The field does not exist making the functionality unavailable
      | + +## 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. From c71d4852cea4a058488a4ce64936bd7915c2406a Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 1 Aug 2023 13:16:52 +0200 Subject: [PATCH 34/36] Changed the value of the anonymous user id --- cmd/create_telemetry.go | 13 +++++++------ telemetry/telemetry.go | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/create_telemetry.go b/cmd/create_telemetry.go index 69bafa84c..7559d1eb9 100644 --- a/cmd/create_telemetry.go +++ b/cmd/create_telemetry.go @@ -18,8 +18,11 @@ import ( ) var ( - CreateUUID = func() string { return uuid.New().String() } - isStdinATTY = term.IsTerminal(int(os.Stdin.Fd())) + CreateUUID = func() string { return uuid.New().String() } + isStdinATTY = term.IsTerminal(int(os.Stdin.Fd())) + anonymousUser = telemetry.User{ + UniqueID: "cli-anonymous-telemetry", + } ) type telemetryUI interface { @@ -125,9 +128,7 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry // If stdin is not available, send telemetry event, disable telemetry and return if !isStdinATTY { settings.IsEnabled = false - err := telemetry.SendTelemetryApproval(telemetry.User{ - UniqueID: settings.UniqueID, - }, telemetry.NoStdin) + err := telemetry.SendTelemetryApproval(anonymousUser, telemetry.NoStdin) if err != nil { fmt.Printf("Error while sending telemetry approval %s\n", err) } @@ -158,7 +159,7 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry } user.UserID = settings.UserID } else { - *user = telemetry.User{} + *user = anonymousUser } // Send telemetry approval event diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index bb16bee0b..434c880ea 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -107,7 +107,7 @@ func newSegmentClient(user User) Client { } if len(user.UniqueID) == 0 { - user.UniqueID = "null" + return CreateNullClient() } err = cli.Enqueue( From 1e32523455924dea941d6b6b70b643459910d985 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Tue, 1 Aug 2023 15:50:13 +0200 Subject: [PATCH 35/36] added event on command and fixed some tests --- cmd/create_telemetry_test.go | 2 + cmd/telemetry.go | 17 +++- cmd/telemetry_test.go | 173 +++++++++-------------------------- cmd/telemetry_unit_test.go | 143 +++++++++++++++++++++++++++++ telemetry/events.go | 14 +++ telemetry/telemetry.go | 5 +- 6 files changed, 217 insertions(+), 137 deletions(-) create mode 100644 cmd/telemetry_unit_test.go diff --git a/cmd/create_telemetry_test.go b/cmd/create_telemetry_test.go index dfddc6150..19c65ac70 100644 --- a/cmd/create_telemetry_test.go +++ b/cmd/create_telemetry_test.go @@ -131,6 +131,7 @@ func TestLoadTelemetrySettings(t *testing.T) { }, telemetryEvents: []telemetry.Event{ {Object: "cli-telemetry", Action: "disabled", Properties: map[string]interface{}{ + "UUID": "cli-anonymous-telemetry", "is_self_hosted": false, }}, }, @@ -182,6 +183,7 @@ func TestLoadTelemetrySettings(t *testing.T) { fileNotCreated: true, telemetryEvents: []telemetry.Event{ {Object: "cli-telemetry", Action: "disabled_default", Properties: map[string]interface{}{ + "UUID": "cli-anonymous-telemetry", "is_self_hosted": false, }}, }, diff --git a/cmd/telemetry.go b/cmd/telemetry.go index 79ad04035..85e231f74 100644 --- a/cmd/telemetry.go +++ b/cmd/telemetry.go @@ -4,6 +4,7 @@ import ( "os" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -14,8 +15,12 @@ func newTelemetryCommand(config *settings.Config) *cobra.Command { telemetryEnable := &cobra.Command{ Use: "enable", Short: "Allow telemetry events to be sent to CircleCI servers", - RunE: func(_ *cobra.Command, _ []string) error { - return setIsTelemetryActive(apiClient, true) + RunE: func(cmd *cobra.Command, _ []string) error { + err := setIsTelemetryActive(apiClient, true) + if telemetryClient, ok := telemetry.FromContext(cmd.Context()); ok { + _ = telemetryClient.Track(telemetry.CreateChangeTelemetryStatusEvent("enabled", "telemetry-command", err)) + } + return err }, Args: cobra.ExactArgs(0), } @@ -23,8 +28,12 @@ func newTelemetryCommand(config *settings.Config) *cobra.Command { telemetryDisable := &cobra.Command{ Use: "disable", Short: "Make sure no telemetry events is sent to CircleCI servers", - RunE: func(_ *cobra.Command, _ []string) error { - return setIsTelemetryActive(apiClient, false) + RunE: func(cmd *cobra.Command, _ []string) error { + err := setIsTelemetryActive(apiClient, false) + if telemetryClient, ok := telemetry.FromContext(cmd.Context()); ok { + _ = telemetryClient.Track(telemetry.CreateChangeTelemetryStatusEvent("disabled", "telemetry-command", err)) + } + return err }, Args: cobra.ExactArgs(0), } diff --git a/cmd/telemetry_test.go b/cmd/telemetry_test.go index 468e1ce9d..fb8d334db 100644 --- a/cmd/telemetry_test.go +++ b/cmd/telemetry_test.go @@ -1,143 +1,58 @@ -package cmd +package cmd_test import ( - "path/filepath" - "testing" + "fmt" + "os/exec" - "github.com/CircleCI-Public/circleci-cli/settings" - "github.com/spf13/afero" - "gotest.tools/v3/assert" -) - -func TestSetIsTelemetryActive(t *testing.T) { - type args struct { - apiClient TelemetryAPIClient - isActive bool - settings *settings.TelemetrySettings - } - type want struct { - settings *settings.TelemetrySettings - } + "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/onsi/gomega/gexec" - type testCase struct { - name string - args args - want want - } + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) - userId := "user-id" - uniqueId := "unique-id" +var _ = Describe("Telemetry events on telemetry commands", func() { + var ( + tempSettings *clitest.TempSettings + command *exec.Cmd + ) - testCases := []testCase{ - { - name: "Enabling telemetry with settings should just update the is active field", - args: args{ - apiClient: telemetryTestAPIClient{}, - isActive: true, - settings: &settings.TelemetrySettings{ - IsEnabled: false, - HasAnsweredPrompt: true, - UniqueID: uniqueId, - UserID: userId, - }, - }, - want: want{ - settings: &settings.TelemetrySettings{ - IsEnabled: true, - HasAnsweredPrompt: true, - UniqueID: uniqueId, - UserID: userId, - }, - }, - }, - { - name: "Enabling telemetry without settings should fill the settings fields", - args: args{ - apiClient: telemetryTestAPIClient{id: userId, err: nil}, - isActive: true, - settings: nil, - }, - want: want{ - settings: &settings.TelemetrySettings{ - IsEnabled: true, - HasAnsweredPrompt: true, - UniqueID: uniqueId, - UserID: userId, - }, - }, - }, - { - name: "Disabling telemetry with settings should just update the is active field", - args: args{ - apiClient: telemetryTestAPIClient{}, - isActive: false, - settings: &settings.TelemetrySettings{ - IsEnabled: true, - HasAnsweredPrompt: true, - UniqueID: uniqueId, - UserID: userId, - }, - }, - want: want{ - settings: &settings.TelemetrySettings{ - IsEnabled: false, - HasAnsweredPrompt: true, - UniqueID: uniqueId, - UserID: userId, - }, - }, - }, - { - name: "Enabling telemetry without settings should fill the settings fields", - args: args{ - apiClient: telemetryTestAPIClient{id: userId, err: nil}, - isActive: false, - settings: nil, - }, - want: want{ - settings: &settings.TelemetrySettings{ - IsEnabled: false, - HasAnsweredPrompt: true, - UniqueID: uniqueId, - UserID: userId, - }, - }, - }, - } + BeforeEach(func() { + tempSettings = clitest.WithTempSettings() + }) - // Mock create UUID - oldUUIDCreate := CreateUUID - CreateUUID = func() string { return uniqueId } - defer (func() { CreateUUID = oldUUIDCreate })() + AfterEach(func() { + tempSettings.Close() + }) - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - // Mock FS - oldFS := settings.FS.Fs - settings.FS.Fs = afero.NewMemMapFs() - defer (func() { settings.FS.Fs = oldFS })() + Describe("telemetry enable", func() { + It("should send an event", func() { + command = exec.Command(pathCLI, "telemetry", "enable") + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) - if tt.args.settings != nil { - err := tt.args.settings.Write() - assert.NilError(t, err) - } + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) - err := setIsTelemetryActive(tt.args.apiClient, tt.args.isActive) - assert.NilError(t, err) + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateChangeTelemetryStatusEvent("enabled", "telemetry-command", nil), + }) + }) + }) - exist, err := settings.FS.Exists(filepath.Join(settings.SettingsPath(), "telemetry.yml")) - assert.NilError(t, err) - if tt.want.settings == nil { - assert.Equal(t, exist, false) - } else { - assert.Equal(t, exist, true) + Describe("telemetry disable", func() { + It("should send an event", func() { + command = exec.Command(pathCLI, "telemetry", "disable") + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) - loadedSettings := &settings.TelemetrySettings{} - err := loadedSettings.Load() - assert.NilError(t, err) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) - assert.DeepEqual(t, tt.want.settings, loadedSettings) - } + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateChangeTelemetryStatusEvent("disabled", "telemetry-command", nil), + }) }) - } -} + }) +}) diff --git a/cmd/telemetry_unit_test.go b/cmd/telemetry_unit_test.go new file mode 100644 index 000000000..468e1ce9d --- /dev/null +++ b/cmd/telemetry_unit_test.go @@ -0,0 +1,143 @@ +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/spf13/afero" + "gotest.tools/v3/assert" +) + +func TestSetIsTelemetryActive(t *testing.T) { + type args struct { + apiClient TelemetryAPIClient + isActive bool + settings *settings.TelemetrySettings + } + type want struct { + settings *settings.TelemetrySettings + } + + type testCase struct { + name string + args args + want want + } + + userId := "user-id" + uniqueId := "unique-id" + + testCases := []testCase{ + { + name: "Enabling telemetry with settings should just update the is active field", + args: args{ + apiClient: telemetryTestAPIClient{}, + isActive: true, + settings: &settings.TelemetrySettings{ + IsEnabled: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsEnabled: true, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + { + name: "Enabling telemetry without settings should fill the settings fields", + args: args{ + apiClient: telemetryTestAPIClient{id: userId, err: nil}, + isActive: true, + settings: nil, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsEnabled: true, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + { + name: "Disabling telemetry with settings should just update the is active field", + args: args{ + apiClient: telemetryTestAPIClient{}, + isActive: false, + settings: &settings.TelemetrySettings{ + IsEnabled: true, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsEnabled: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + { + name: "Enabling telemetry without settings should fill the settings fields", + args: args{ + apiClient: telemetryTestAPIClient{id: userId, err: nil}, + isActive: false, + settings: nil, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsEnabled: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + } + + // Mock create UUID + oldUUIDCreate := CreateUUID + CreateUUID = func() string { return uniqueId } + defer (func() { CreateUUID = oldUUIDCreate })() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + // Mock FS + oldFS := settings.FS.Fs + settings.FS.Fs = afero.NewMemMapFs() + defer (func() { settings.FS.Fs = oldFS })() + + if tt.args.settings != nil { + err := tt.args.settings.Write() + assert.NilError(t, err) + } + + err := setIsTelemetryActive(tt.args.apiClient, tt.args.isActive) + assert.NilError(t, err) + + exist, err := settings.FS.Exists(filepath.Join(settings.SettingsPath(), "telemetry.yml")) + assert.NilError(t, err) + if tt.want.settings == nil { + assert.Equal(t, exist, false) + } else { + assert.Equal(t, exist, true) + + loadedSettings := &settings.TelemetrySettings{} + err := loadedSettings.Load() + assert.NilError(t, err) + + assert.DeepEqual(t, tt.want.settings, loadedSettings) + } + }) + } +} diff --git a/telemetry/events.go b/telemetry/events.go index 2985364e6..dfdc61330 100644 --- a/telemetry/events.go +++ b/telemetry/events.go @@ -133,3 +133,17 @@ func CreateInfoEvent(cmdInfo CommandInfo, err error) Event { } return event } + +func CreateChangeTelemetryStatusEvent(action string, origin string, err error) Event { + event := Event{ + Object: "cli-telemetry", + Action: action, + Properties: map[string]interface{}{ + "origin": origin, + }, + } + if err != nil { + event.Properties["error"] = err.Error() + } + return event +} diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 434c880ea..928e59dd8 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -63,10 +63,7 @@ func SendTelemetryApproval(user User, approval Approval) error { client := CreateActiveTelemetry(user) defer client.Close() - return client.Track(Event{ - Object: "cli-telemetry", - Action: string(approval), - }) + return client.Track(CreateChangeTelemetryStatusEvent(string(approval), "first-time-prompt", nil)) } // Null client From 3c80dfe8c0e9a9dae7437406a6961e56434ea89d Mon Sep 17 00:00:00 2001 From: Benedetta Dal Canton Date: Wed, 2 Aug 2023 11:30:51 +0200 Subject: [PATCH 36/36] Chore: Update telemetry section in README/ --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c8fd82bc1..b57b73873 100644 --- a/README.md +++ b/README.md @@ -187,4 +187,13 @@ Please see the [documentation](https://circleci-public.github.io/circleci-cli) o ## 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. +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 +