Skip to content

Commit 49672ea

Browse files
authored
[WEBXP-315] refactor and expand telemetry for the CLI (#1183)
1 parent d72a81c commit 49672ea

33 files changed

+867
-100
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
* @CircleCI-Public/lifecycle
2-
*orb*.go @CircleCI-Public/orb-publishers @CircleCI-Public/lifecycle
1+
* @circleci/webex
2+
*orb*.go @circleci/webex
33

44
/api/runner @CircleCI-Public/on-prem
55
/cmd/runner @CircleCI-Public/on-prem

api/api.go

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/CircleCI-Public/circleci-cli/api/graphql"
1919
"github.com/CircleCI-Public/circleci-cli/api/rest"
20+
"github.com/CircleCI-Public/circleci-cli/errs"
2021
"github.com/CircleCI-Public/circleci-cli/references"
2122
"github.com/CircleCI-Public/circleci-cli/settings"
2223
)
@@ -54,6 +55,10 @@ func (errs GQLErrorsCollection) Error() string {
5455
return strings.Join(messages, "\n")
5556
}
5657

58+
func wrapGQLErrors(gqlErrors GQLErrorsCollection) error {
59+
return errs.CheckAuthRequired(gqlErrors)
60+
}
61+
5762
// GQLResponseError is a mapping of the data returned by the GraphQL server of key-value pairs.
5863
// Typically used with the structure "Message: string", but other response errors provide additional fields.
5964
type GQLResponseError struct {
@@ -543,7 +548,7 @@ func OrbImportVersion(cl *graphql.Client, orbSrc string, orbID string, orbVersio
543548
}
544549

545550
if len(response.ImportOrbVersion.Errors) > 0 {
546-
return nil, response.ImportOrbVersion.Errors
551+
return nil, wrapGQLErrors(response.ImportOrbVersion.Errors)
547552
}
548553

549554
return &response.ImportOrbVersion.Orb, nil
@@ -590,7 +595,7 @@ func OrbPublishByName(cl *graphql.Client, configPath, orbName, namespaceName, or
590595
}
591596

592597
if len(response.PublishOrb.Errors) > 0 {
593-
return nil, response.PublishOrb.Errors
598+
return nil, wrapGQLErrors(response.PublishOrb.Errors)
594599
}
595600

596601
return &response.PublishOrb.Orb, nil
@@ -663,7 +668,7 @@ func OrbID(cl *graphql.Client, namespace string, orb string) (*OrbIDResponse, er
663668
return nil, namespaceNotFound(namespace)
664669
}
665670

666-
return nil, fmt.Errorf("the '%s' orb does not exist in the '%s' namespace. Did you misspell the namespace or the orb name?", orb, namespace)
671+
return nil, errs.NotFoundf("the '%s' orb does not exist in the '%s' namespace. Did you misspell the namespace or the orb name?", orb, namespace)
667672
}
668673

669674
// CreateImportedNamespace creates an imported namespace with the provided name. An imported namespace
@@ -697,7 +702,7 @@ func CreateImportedNamespace(cl *graphql.Client, name string) (*ImportNamespaceR
697702
}
698703

699704
if len(response.ImportNamespace.Errors) > 0 {
700-
return nil, response.ImportNamespace.Errors
705+
return nil, wrapGQLErrors(response.ImportNamespace.Errors)
701706
}
702707

703708
return &response, nil
@@ -731,7 +736,7 @@ func CreateNamespaceWithOwnerID(cl *graphql.Client, name string, ownerID string)
731736
err := cl.Run(request, &response)
732737

733738
if len(response.CreateNamespace.Errors) > 0 {
734-
return nil, response.CreateNamespace.Errors
739+
return nil, wrapGQLErrors(response.CreateNamespace.Errors)
735740
}
736741

737742
if err != nil {
@@ -828,7 +833,7 @@ mutation($name: String!) {
828833
}
829834

830835
if len(response.DeleteNamespaceAlias.Errors) > 0 {
831-
return response.DeleteNamespaceAlias.Errors
836+
return wrapGQLErrors(response.DeleteNamespaceAlias.Errors)
832837
}
833838

834839
if !response.DeleteNamespaceAlias.Deleted {
@@ -866,7 +871,7 @@ mutation($id: UUID!) {
866871
}
867872

868873
if len(response.DeleteNamespace.Errors) > 0 {
869-
return response.DeleteNamespace.Errors
874+
return wrapGQLErrors(response.DeleteNamespace.Errors)
870875
}
871876

872877
if !response.DeleteNamespace.Deleted {
@@ -977,7 +982,7 @@ func renameNamespaceWithNsID(cl *graphql.Client, id, newName string) (*RenameNam
977982
err := cl.Run(request, &response)
978983

979984
if len(response.RenameNamespace.Errors) > 0 {
980-
return nil, response.RenameNamespace.Errors
985+
return nil, wrapGQLErrors(response.RenameNamespace.Errors)
981986
}
982987

983988
if err != nil {
@@ -1024,7 +1029,7 @@ func createOrbWithNsID(cl *graphql.Client, name string, namespaceID string, isPr
10241029
err := cl.Run(request, &response)
10251030

10261031
if len(response.CreateOrb.Errors) > 0 {
1027-
return nil, response.CreateOrb.Errors
1032+
return nil, wrapGQLErrors(response.CreateOrb.Errors)
10281033
}
10291034

10301035
if err != nil {
@@ -1080,7 +1085,7 @@ func CreateImportedOrb(cl *graphql.Client, namespace string, name string) (*Impo
10801085
}
10811086

10821087
if len(response.ImportOrb.Errors) > 0 {
1083-
return nil, response.ImportOrb.Errors
1088+
return nil, wrapGQLErrors(response.ImportOrb.Errors)
10841089
}
10851090

10861091
return &response, nil
@@ -1201,7 +1206,7 @@ func OrbPromoteByName(cl *graphql.Client, namespaceName, orbName, label, segment
12011206
err = cl.Run(request, &response)
12021207

12031208
if len(response.PromoteOrb.Errors) > 0 {
1204-
return nil, response.PromoteOrb.Errors
1209+
return nil, wrapGQLErrors(response.PromoteOrb.Errors)
12051210
}
12061211

12071212
if err != nil {
@@ -1244,7 +1249,7 @@ mutation($orbId: UUID!, $list: Boolean!) {
12441249
err = cl.Run(request, &response)
12451250

12461251
if len(response.SetOrbListStatus.Errors) > 0 {
1247-
return nil, response.SetOrbListStatus.Errors
1252+
return nil, wrapGQLErrors(response.SetOrbListStatus.Errors)
12481253
}
12491254

12501255
if err != nil {
@@ -1298,7 +1303,7 @@ func OrbSource(cl *graphql.Client, orbRef string) (string, error) {
12981303
}
12991304

13001305
if response.OrbVersion.ID == "" {
1301-
return "", fmt.Errorf("no Orb '%s' was found; please check that the Orb reference is correct", orbRef)
1306+
return "", &ErrOrbVersionNotExists{OrbRef: orbRef}
13021307
}
13031308

13041309
return response.OrbVersion.Source, nil
@@ -1315,6 +1320,8 @@ func (e *ErrOrbVersionNotExists) Error() string {
13151320
return fmt.Sprintf("no Orb '%s' was found; please check that the Orb reference is correct", e.OrbRef)
13161321
}
13171322

1323+
func (e *ErrOrbVersionNotExists) Is(target error) bool { return target == errs.ErrNotFound }
1324+
13181325
// OrbInfo gets the meta-data of an orb
13191326
func OrbInfo(cl *graphql.Client, orbRef string) (*OrbVersion, error) {
13201327
if err := references.IsOrbRefWithOptionalVersion(orbRef); err != nil {
@@ -1714,7 +1721,7 @@ func AddOrRemoveOrbCategorization(cl *graphql.Client, namespace string, orb stri
17141721
responseData := response[mutationName]
17151722

17161723
if len(responseData.Errors) > 0 {
1717-
return &responseData.Errors
1724+
return wrapGQLErrors(responseData.Errors)
17181725
}
17191726

17201727
if err != nil {

api/context/gql_client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/CircleCI-Public/circleci-cli/api"
77
"github.com/CircleCI-Public/circleci-cli/api/graphql"
8+
"github.com/CircleCI-Public/circleci-cli/errs"
89
)
910

1011
type gqlClient struct {
@@ -53,7 +54,7 @@ func (c *gqlClient) ContextByName(name string) (Context, error) {
5354
return context, nil
5455
}
5556
}
56-
return Context{}, errors.New("No context found with that name")
57+
return Context{}, errs.NotFoundf("No context found with that name")
5758
}
5859

5960
func (c *gqlClient) CreateContext(name string) error {

api/context/rest_client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55

66
"github.com/CircleCI-Public/circleci-cli/api/rest"
7+
"github.com/CircleCI-Public/circleci-cli/errs"
78
)
89

910
type restClient struct {
@@ -64,7 +65,7 @@ func (c restClient) ContextByName(name string) (Context, error) {
6465

6566
params.PageToken = resp.NextPageToken
6667
}
67-
return Context{}, fmt.Errorf("context with name %s not found", name)
68+
return Context{}, errs.NotFoundf("context with name %s not found", name)
6869
}
6970

7071
func (c restClient) CreateContext(name string) error {

api/graphql/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strings"
1414

1515
"github.com/CircleCI-Public/circleci-cli/api/header"
16+
"github.com/CircleCI-Public/circleci-cli/errs"
1617
"github.com/CircleCI-Public/circleci-cli/version"
1718
"github.com/pkg/errors"
1819
)
@@ -275,7 +276,7 @@ func (cl *Client) Run(request *Request, resp interface{}) error {
275276
}
276277

277278
if len(wrappedResponse.Errors) > 0 {
278-
return wrappedResponse.Errors
279+
return errs.CheckAuthRequired(wrappedResponse.Errors)
279280
}
280281

281282
return nil

api/runner/runner.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/CircleCI-Public/circleci-cli/api/rest"
10+
"github.com/CircleCI-Public/circleci-cli/errs"
1011
)
1112

1213
type Runner struct {
@@ -56,7 +57,7 @@ func (r *Runner) GetResourceClassByName(resourceClass string) (rc *ResourceClass
5657
}
5758
}
5859

59-
return nil, fmt.Errorf("resource class %q not found", resourceClass)
60+
return nil, errs.NotFoundf("resource class %q not found", resourceClass)
6061
}
6162

6263
func (r *Runner) GetNamespaceByResourceClass(resourceClass string) (ns string, err error) {

clitest/telemetry.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"github.com/onsi/gomega"
99
)
1010

11+
// CompareTelemetryEvent asserts that the recorded telemetry events exactly
12+
// match `expected`. Use this when you need strict ordering and field equality.
1113
func CompareTelemetryEvent(settings *TempSettings, expected []telemetry.Event) {
1214
content, err := os.ReadFile(settings.TelemetryDestPath)
1315
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
@@ -17,3 +19,28 @@ func CompareTelemetryEvent(settings *TempSettings, expected []telemetry.Event) {
1719
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
1820
gomega.Expect(result).To(gomega.Equal(expected))
1921
}
22+
23+
// ReadTelemetryEvents reads and parses all telemetry events from the mock file.
24+
func ReadTelemetryEvents(settings *TempSettings) []telemetry.Event {
25+
content, err := os.ReadFile(settings.TelemetryDestPath)
26+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
27+
28+
var result []telemetry.Event
29+
err = json.Unmarshal(content, &result)
30+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
31+
return result
32+
}
33+
34+
// CompareTelemetryEventSubset asserts that every event in `expected` appears
35+
// somewhere in the recorded events. Use this when the middleware also emits
36+
// cli_command_started / cli_command_finished events alongside legacy events
37+
// and you only want to verify the legacy event payload.
38+
func CompareTelemetryEventSubset(settings *TempSettings, expected []telemetry.Event) {
39+
content, err := os.ReadFile(settings.TelemetryDestPath)
40+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
41+
42+
result := []telemetry.Event{}
43+
err = json.Unmarshal(content, &result)
44+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
45+
gomega.Expect(result).To(gomega.ContainElements(expected))
46+
}

cmd/config_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ var _ = Describe("Config", func() {
5050
Expect(err).ShouldNot(HaveOccurred())
5151

5252
Eventually(session).Should(gexec.Exit(0))
53-
clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
53+
clitest.CompareTelemetryEventSubset(tempSettings, []telemetry.Event{
5454
telemetry.CreateConfigEvent(telemetry.CommandInfo{
5555
Name: "pack",
56-
LocalArgs: map[string]string{"help": "false"},
56+
LocalArgs: map[string]string{},
5757
}, nil),
5858
})
5959
})

cmd/create_telemetry.go

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/CircleCI-Public/circleci-cli/version"
1414
"github.com/google/uuid"
1515
"github.com/spf13/cobra"
16-
"github.com/spf13/pflag"
1716
"golang.org/x/term"
1817
)
1918

@@ -180,27 +179,9 @@ func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry
180179
}
181180
}
182181

183-
// Utility function used when creating telemetry events.
184-
// It takes a cobra Command and creates a telemetry.CommandInfo of it
185-
// If getParent is true, puts both the command's args in `LocalArgs` and the parent's args
186-
// Else only put the command's args
187-
// Note: child flags overwrite parent flags with same name
182+
// GetCommandInformation takes a cobra Command and creates a telemetry.CommandInfo.
183+
// If getParent is true, it also includes explicitly-set flags from the parent command.
184+
// Only flags explicitly set by the user are included; sensitive flags are redacted.
188185
func GetCommandInformation(cmd *cobra.Command, getParent bool) telemetry.CommandInfo {
189-
localArgs := map[string]string{}
190-
191-
parent := cmd.Parent()
192-
if getParent && parent != nil {
193-
parent.LocalFlags().VisitAll(func(flag *pflag.Flag) {
194-
localArgs[flag.Name] = flag.Value.String()
195-
})
196-
}
197-
198-
cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) {
199-
localArgs[flag.Name] = flag.Value.String()
200-
})
201-
202-
return telemetry.CommandInfo{
203-
Name: cmd.Name(),
204-
LocalArgs: localArgs,
205-
}
186+
return telemetry.GetCommandInformation(cmd, getParent)
206187
}

cmd/diagnostic_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ var _ = Describe("Diagnostic", func() {
5959
Expect(err).ShouldNot(HaveOccurred())
6060

6161
Eventually(session).Should(gexec.Exit(0))
62-
clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
62+
clitest.CompareTelemetryEventSubset(tempSettings, []telemetry.Event{
6363
telemetry.CreateDiagnosticEvent(nil),
6464
})
6565
})

0 commit comments

Comments
 (0)