Skip to content

Commit 5b6c63d

Browse files
authored
Add (out-out) telemetry to CLI (#19033)
* Rename AddApology to MarkExceptional * Add telemetry * Improve login failure behaviour * Fix list format * Generate identity based on the MAC
1 parent 70a0f12 commit 5b6c63d

File tree

18 files changed

+306
-40
lines changed

18 files changed

+306
-40
lines changed

components/local-app/BUILD.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const generatePackage = function (goos, goarch, binaryName, mainFile) {
2323
"build",
2424
"-trimpath",
2525
"-ldflags",
26-
"-buildid= -w -s -X 'github.com/gitpod-io/local-app/pkg/common.Version=commit-${__git_commit}'",
26+
"-buildid= -w -s -X 'github.com/gitpod-io/local-app/pkg/constants.Version=commit-${__git_commit}'",
2727
"-o",
2828
binaryName,
2929
mainFile,

components/local-app/cmd/login.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cmd
66

77
import (
88
"context"
9+
"errors"
910
"fmt"
1011
"log/slog"
1112
"net/url"
@@ -88,17 +89,28 @@ var loginCmd = &cobra.Command{
8889
}
8990
orgsList, err := clnt.Teams.ListTeams(cmd.Context(), connect.NewRequest(&v1.ListTeamsRequest{}))
9091
if err != nil {
91-
resolutions := []string{
92-
"pass an organization ID using --organization-id",
92+
var (
93+
resolutions []string
94+
unauthenticated bool
95+
)
96+
if ce := new(connect.Error); errors.As(err, &ce) && ce.Code() == connect.CodeUnauthenticated {
97+
unauthenticated = true
98+
resolutions = []string{
99+
"pass an organization ID using --organization-id",
100+
}
101+
if loginOpts.Token != "" {
102+
resolutions = append(resolutions,
103+
"make sure the token has the right scopes",
104+
"use a different token",
105+
"login without passing a token but using the browser instead",
106+
)
107+
}
93108
}
94-
if loginOpts.Token != "" {
95-
resolutions = append(resolutions,
96-
"make sure the token has the right scopes",
97-
"use a different token",
98-
"login without passing a token but using the browser instead",
99-
)
109+
if unauthenticated {
110+
return prettyprint.AddResolution(fmt.Errorf("unauthenticated"), resolutions...)
111+
} else {
112+
return prettyprint.MarkExceptional(err)
100113
}
101-
return prettyprint.AddResolution(fmt.Errorf("cannot list organizations: %w", err), resolutions...)
102114
}
103115

104116
var orgID string
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package cmd
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"net/http"
11+
"testing"
12+
13+
"github.com/bufbuild/connect-go"
14+
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
15+
gitpod_experimental_v1connect "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
16+
"github.com/gitpod-io/local-app/pkg/config"
17+
)
18+
19+
func TestLoginCmd(t *testing.T) {
20+
RunCommandTests(t, []CommandTest{
21+
{
22+
Name: "test unauthenticated",
23+
Commandline: []string{"login", "--token", "foo"},
24+
Config: &config.Config{
25+
ActiveContext: "test",
26+
},
27+
PrepServer: func(mux *http.ServeMux) {
28+
mux.Handle(gitpod_experimental_v1connect.NewTeamsServiceHandler(&testLoginCmdSrv{
29+
Err: connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("cannot establish caller identity")),
30+
}))
31+
},
32+
Expectation: CommandTestExpectation{
33+
Error: "unauthenticated",
34+
HasResolutions: true,
35+
},
36+
},
37+
})
38+
}
39+
40+
type testLoginCmdSrv struct {
41+
Err error
42+
gitpod_experimental_v1connect.UnimplementedTeamsServiceHandler
43+
}
44+
45+
func (srv testLoginCmdSrv) ListTeams(context.Context, *connect.Request[v1.ListTeamsRequest]) (*connect.Response[v1.ListTeamsResponse], error) {
46+
return nil, srv.Err
47+
}

components/local-app/cmd/root.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import (
1717
"github.com/gitpod-io/gitpod/components/public-api/go/client"
1818
"github.com/gitpod-io/local-app/pkg/auth"
1919
"github.com/gitpod-io/local-app/pkg/config"
20+
"github.com/gitpod-io/local-app/pkg/constants"
2021
"github.com/gitpod-io/local-app/pkg/prettyprint"
22+
"github.com/gitpod-io/local-app/pkg/telemetry"
2123
"github.com/gookit/color"
2224
"github.com/lmittmann/tint"
2325
"github.com/mattn/go-isatty"
@@ -88,17 +90,33 @@ var rootCmd = &cobra.Command{
8890
return err
8991
}
9092
cmd.SetContext(config.ToContext(context.Background(), cfg))
93+
94+
telemetryEnabled := !telemetry.DoNotTrack()
95+
telemetryEnabled = telemetryEnabled && cfg.Telemetry.Enabled
96+
// For now we only enable telemetry on gitpod.io
97+
if gpctx, err := cfg.GetActiveContext(); err == nil && gpctx != nil {
98+
telemetryEnabled = telemetryEnabled && gpctx.Host.String() == "https://gitpod.io"
99+
}
100+
telemetry.Init(telemetryEnabled, cfg.Telemetry.Identity, constants.Version)
101+
telemetry.RecordCommand(cmd)
102+
91103
return nil
92104
},
93105
}
94106

95107
func Execute() {
96108
err := rootCmd.Execute()
109+
110+
var exitCode int
97111
if err != nil {
112+
exitCode = 1
98113
prettyprint.PrintError(os.Stderr, os.Args[0], err)
99114

100-
os.Exit(1)
115+
telemetry.RecordError(err)
101116
}
117+
118+
telemetry.Close()
119+
os.Exit(exitCode)
102120
}
103121

104122
func init() {
@@ -141,7 +159,7 @@ func getGitpodClient(ctx context.Context) (*client.Gitpod, error) {
141159
)
142160
}
143161

144-
if host.String() == "https://testing" && rootTestingOpts.Client != nil {
162+
if rootTestingOpts.Client != nil {
145163
return rootTestingOpts.Client, nil
146164
}
147165

components/local-app/cmd/root_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cmd
66

77
import (
88
"bytes"
9+
"errors"
910
"net/http"
1011
"net/http/httptest"
1112
"net/url"
@@ -16,6 +17,7 @@ import (
1617
"github.com/gitpod-io/gitpod/components/public-api/go/client"
1718
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
1819
"github.com/gitpod-io/local-app/pkg/config"
20+
"github.com/gitpod-io/local-app/pkg/prettyprint"
1921
"github.com/google/go-cmp/cmp"
2022
)
2123

@@ -28,7 +30,10 @@ type CommandTest struct {
2830
}
2931

3032
type CommandTestExpectation struct {
31-
Error string
33+
Error string
34+
SystemException bool
35+
HasResolutions bool
36+
3237
Output string
3338
}
3439

@@ -88,6 +93,12 @@ func RunCommandTests(t *testing.T, tests []CommandTest) {
8893
var act CommandTestExpectation
8994
if err != nil {
9095
act.Error = err.Error()
96+
if se := new(prettyprint.ErrSystemException); errors.As(err, &se) {
97+
act.SystemException = true
98+
}
99+
if re := new(prettyprint.ErrResolution); errors.As(err, &re) {
100+
act.HasResolutions = true
101+
}
91102
}
92103
act.Output = actual.String()
93104

components/local-app/cmd/workspace-create.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ var workspaceCreateCmd = &cobra.Command{
3939
if workspaceCreateOpts.WorkspaceClass != "" {
4040
resp, err := gitpod.Workspaces.ListWorkspaceClasses(cmd.Context(), connect.NewRequest(&v1.ListWorkspaceClassesRequest{}))
4141
if err != nil {
42-
return prettyprint.AddApology(prettyprint.AddResolution(fmt.Errorf("cannot list workspace classes: %w", err),
42+
return prettyprint.MarkExceptional(prettyprint.AddResolution(fmt.Errorf("cannot list workspace classes: %w", err),
4343
"don't pass an explicit workspace class, i.e. omit the --class flag",
4444
))
4545
}
@@ -63,7 +63,7 @@ var workspaceCreateCmd = &cobra.Command{
6363
if workspaceCreateOpts.Editor != "" {
6464
resp, err := gitpod.Editors.ListEditorOptions(cmd.Context(), connect.NewRequest(&v1.ListEditorOptionsRequest{}))
6565
if err != nil {
66-
return prettyprint.AddApology(prettyprint.AddResolution(fmt.Errorf("cannot list editor options: %w", err),
66+
return prettyprint.MarkExceptional(prettyprint.AddResolution(fmt.Errorf("cannot list editor options: %w", err),
6767
"don't pass an explicit editor, i.e. omit the --editor flag",
6868
))
6969
}
@@ -109,7 +109,7 @@ var workspaceCreateCmd = &cobra.Command{
109109

110110
workspaceID := newWorkspace.Msg.WorkspaceId
111111
if len(workspaceID) == 0 {
112-
return prettyprint.AddApology(prettyprint.AddResolution(fmt.Errorf("workspace was not created"),
112+
return prettyprint.MarkExceptional(prettyprint.AddResolution(fmt.Errorf("workspace was not created"),
113113
"try to create the workspace again",
114114
))
115115
}

components/local-app/cmd/workspace-list-classes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ var workspaceListClassesCmd = &cobra.Command{
4343
})
4444
}
4545

46-
return WriteTabular(res, workspaceListClassesOpts.Format, prettyprint.WriterFormatNarrow)
46+
return WriteTabular(res, workspaceListClassesOpts.Format, prettyprint.WriterFormatWide)
4747
},
4848
}
4949

components/local-app/cmd/workspace-list-editors.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ var workspaceListEditors = &cobra.Command{
4848
})
4949
}
5050

51-
return WriteTabular(res, workspaceListEditorsOpts.Format, prettyprint.WriterFormatNarrow)
51+
return WriteTabular(res, workspaceListEditorsOpts.Format, prettyprint.WriterFormatWide)
5252
},
5353
}
5454

components/local-app/cmd/workspace-list_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ func TestWorkspaceListCmd(t *testing.T) {
2020
{
2121
Name: "no config",
2222
Commandline: []string{"workspace", "list"},
23-
Expectation: CommandTestExpectation{Error: config.ErrNoContext.Error()},
23+
Expectation: CommandTestExpectation{
24+
Error: config.ErrNoContext.Error(),
25+
HasResolutions: true,
26+
},
2427
},
2528
{
2629
Name: "test one workspace",

components/local-app/cmd/workspace-up.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ var workspaceUpCmd = &cobra.Command{
5656
if workspaceCreateOpts.WorkspaceClass != "" {
5757
resp, err := gitpod.Workspaces.ListWorkspaceClasses(cmd.Context(), connect.NewRequest(&v1.ListWorkspaceClassesRequest{}))
5858
if err != nil {
59-
return prettyprint.AddApology(prettyprint.AddResolution(fmt.Errorf("cannot list workspace classes: %w", err),
59+
return prettyprint.MarkExceptional(prettyprint.AddResolution(fmt.Errorf("cannot list workspace classes: %w", err),
6060
"don't pass an explicit workspace class, i.e. omit the --class flag",
6161
))
6262
}
@@ -80,7 +80,7 @@ var workspaceUpCmd = &cobra.Command{
8080
if workspaceCreateOpts.Editor != "" {
8181
resp, err := gitpod.Editors.ListEditorOptions(cmd.Context(), connect.NewRequest(&v1.ListEditorOptionsRequest{}))
8282
if err != nil {
83-
return prettyprint.AddApology(prettyprint.AddResolution(fmt.Errorf("cannot list editor options: %w", err),
83+
return prettyprint.MarkExceptional(prettyprint.AddResolution(fmt.Errorf("cannot list editor options: %w", err),
8484
"don't pass an explicit editor, i.e. omit the --editor flag",
8585
))
8686
}
@@ -109,7 +109,7 @@ var workspaceUpCmd = &cobra.Command{
109109
defer func() {
110110
// If the error doesn't have a resolution, assume it's a system error and add an apology
111111
if err != nil && !errors.Is(err, &prettyprint.ErrResolution{}) {
112-
err = prettyprint.AddApology(err)
112+
err = prettyprint.MarkExceptional(err)
113113
}
114114
}()
115115

@@ -143,12 +143,12 @@ var workspaceUpCmd = &cobra.Command{
143143
slog.Debug("found Git working copy", "dir", currentDir)
144144
repo, err := git.PlainOpen(currentDir)
145145
if err != nil {
146-
return prettyprint.AddApology(fmt.Errorf("cannot open Git working copy at %s: %w", currentDir, err))
146+
return prettyprint.MarkExceptional(fmt.Errorf("cannot open Git working copy at %s: %w", currentDir, err))
147147
}
148148
_ = repo.DeleteRemote("gitpod")
149149
head, err := repo.Head()
150150
if err != nil {
151-
return prettyprint.AddApology(fmt.Errorf("cannot get HEAD: %w", err))
151+
return prettyprint.MarkExceptional(fmt.Errorf("cannot get HEAD: %w", err))
152152
}
153153
branch := head.Name().Short()
154154

@@ -170,7 +170,7 @@ var workspaceUpCmd = &cobra.Command{
170170
}
171171
workspaceID := newWorkspace.Msg.WorkspaceId
172172
if len(workspaceID) == 0 {
173-
return prettyprint.AddApology(prettyprint.AddResolution(fmt.Errorf("workspace was not created"),
173+
return prettyprint.MarkExceptional(prettyprint.AddResolution(fmt.Errorf("workspace was not created"),
174174
"try to create the workspace again",
175175
))
176176
}

components/local-app/go.mod

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,14 @@ require (
5858
github.com/pkg/errors v0.9.1 // indirect
5959
github.com/pkg/sftp v1.13.5 // indirect
6060
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
61+
github.com/segmentio/backo-go v1.0.0 // indirect
6162
github.com/sergi/go-diff v1.1.0 // indirect
6263
github.com/skeema/knownhosts v1.2.0 // indirect
6364
github.com/stretchr/objx v0.5.0 // indirect
6465
github.com/xanzy/ssh-agent v0.3.3 // indirect
6566
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
66-
golang.org/x/mod v0.12.0 // indirect
67-
golang.org/x/tools v0.13.0 // indirect
67+
golang.org/x/mod v0.13.0 // indirect
68+
golang.org/x/tools v0.14.0 // indirect
6869
gopkg.in/warnings.v0 v0.1.2 // indirect
6970
)
7071

@@ -81,10 +82,11 @@ require (
8182
github.com/klauspost/compress v1.17.0 // indirect
8283
github.com/melbahja/goph v1.4.0
8384
github.com/russross/blackfriday/v2 v2.1.0 // indirect
85+
github.com/segmentio/analytics-go/v3 v3.3.0
8486
github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 // indirect
8587
github.com/spf13/pflag v1.0.5 // indirect
8688
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
87-
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
89+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
8890
golang.org/x/net v0.17.0 // indirect
8991
golang.org/x/sys v0.13.0
9092
golang.org/x/text v0.13.0 // indirect

components/local-app/go.sum

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/local-app/pkg/auth/auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func fetchValidCLIScopes(ctx context.Context, serviceURL string) ([]string, erro
6565
return authScopesLocalCompanion, nil
6666
}
6767

68-
return nil, prettyprint.AddApology(errors.New(serviceURL + " did not provide valid scopes"))
68+
return nil, prettyprint.MarkExceptional(errors.New(serviceURL + " did not provide valid scopes"))
6969
}
7070

7171
type ErrInvalidGitpodToken struct {

0 commit comments

Comments
 (0)