Skip to content

Commit

Permalink
Allow passing caller to telemetry (#6223)
Browse files Browse the repository at this point in the history
* Add integration test cases highlighting the acceptance criteria

* Set the caller property for telemetry based on the 'TELEMETRY_CALLER' env var

Note that this property is set in telemetry even if it is not in the set
of allowed values, but does not prevent the odo command from running.

* Update documentation
  • Loading branch information
rm3l authored Oct 17, 2022
1 parent 8c94844 commit 1c9d6b3
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 32 deletions.
19 changes: 10 additions & 9 deletions docs/website/docs/overview/configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,13 @@ Options here are mostly used for debugging and testing `odo` behavior.

### Environment variables controlling odo behavior

| Variable | Usage |
| -------------------------- |---------------------------------------------------------------------------------------------------------------------|
| `PODMAN_CMD` | The command executed to run the local podman binary. `podman` by default |
| `DOCKER_CMD` | The command executed to run the local docker binary. `docker` by default |
| `ODO_LOG_LEVEL` | Useful for setting a log level to be used by odo commands. |
| `ODO_DISABLE_TELEMETRY` | Useful for disabling telemetry collection. |
| `GLOBALODOCONFIG` | Useful for setting a different location of global preference file preference.yaml. |
| `ODO_DEBUG_TELEMETRY_FILE` | Useful for debugging telemetry. When set it will save telemetry data to a file instead of sending it to the server. |
| `DEVFILE_PROXY` | Integration tests will use this address as Devfile registry instead of `https://registry.stage.devfile.io` |
| Variable | Usage |
|-----------------------------|---------------------------------------------------------------------------------------------------------------------|
| `PODMAN_CMD` | The command executed to run the local podman binary. `podman` by default |
| `DOCKER_CMD` | The command executed to run the local docker binary. `docker` by default |
| `ODO_LOG_LEVEL` | Useful for setting a log level to be used by odo commands. |
| `ODO_DISABLE_TELEMETRY` | Useful for disabling telemetry collection. |
| `GLOBALODOCONFIG` | Useful for setting a different location of global preference file preference.yaml. |
| `ODO_DEBUG_TELEMETRY_FILE` | Useful for debugging telemetry. When set it will save telemetry data to a file instead of sending it to the server. |
| `DEVFILE_PROXY` | Integration tests will use this address as Devfile registry instead of `https://registry.stage.devfile.io` |
| `TELEMETRY_CALLER` | Caller identifier passed to telemetry. Acceptable values: `vscode`, `intellij`, `jboss`. |
5 changes: 5 additions & 0 deletions pkg/odo/genericclioptions/runnable.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ func GenericRun(o Runnable, cmd *cobra.Command, args []string) {
klog.V(4).Infof("WARNING: debug telemetry, if enabled, will be logged in %s", debugTelemetry)
}

err = scontext.SetCaller(cmd.Context(), os.Getenv(segment.TelemetryCaller))
if err != nil {
klog.V(3).Infof("error handling caller property for telemetry: %v", err)
}

scontext.SetFlags(cmd.Context(), cmd.Flags())
// set value for telemetry status in context so that we do not need to call IsTelemetryEnabled every time to check its status
scontext.SetTelemetryStatus(cmd.Context(), segment.IsTelemetryEnabled(cfg))
Expand Down
29 changes: 28 additions & 1 deletion pkg/segment/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import (
"strings"
"sync"

"github.com/redhat-developer/odo/pkg/kclient"
"github.com/spf13/pflag"

"github.com/redhat-developer/odo/pkg/kclient"

dfutil "github.com/devfile/library/pkg/util"

"k8s.io/klog"
)

const (
Caller = "caller"
ComponentType = "componentType"
ClusterType = "clusterType"
TelemetryStatus = "isTelemetryEnabled"
Expand All @@ -27,6 +29,12 @@ const (
Flags = "flags"
)

const (
VSCode = "vscode"
IntelliJ = "intellij"
JBoss = "jboss"
)

type contextKey struct{}

var key = contextKey{}
Expand Down Expand Up @@ -130,6 +138,25 @@ func SetFlags(ctx context.Context, flags *pflag.FlagSet) {
setContextProperty(ctx, Flags, strings.Join(changedFlags, " "))
}

// SetCaller sets the caller property for telemetry to record the tool used to call odo.
// Passing an empty caller is not considered invalid, but means that odo was invoked directly from the command line.
// In all other cases, the value is verified against a set of allowed values.
// Also note that unexpected values are added to the telemetry context, even if an error is returned.
func SetCaller(ctx context.Context, caller string) error {
var err error
s := strings.TrimSpace(strings.ToLower(caller))
switch s {
case "", VSCode, IntelliJ, JBoss:
// An empty caller means that odo was invoked directly from the command line
err = nil
default:
// Note: we purposely don't disclose the list of allowed values
err = fmt.Errorf("unknown caller type: %q", caller)
}
setContextProperty(ctx, Caller, s)
return err
}

// GetTelemetryStatus gets the telemetry status that is set before a command is run
func GetTelemetryStatus(ctx context.Context) bool {
isEnabled, ok := GetContextProperties(ctx)[TelemetryStatus]
Expand Down
59 changes: 59 additions & 0 deletions pkg/segment/context/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package context

import (
"context"
"fmt"
"reflect"
"strings"
"testing"

"github.com/spf13/pflag"
Expand Down Expand Up @@ -201,3 +203,60 @@ func TestSetFlags(t *testing.T) {
})
}
}

func TestSetCaller(t *testing.T) {
type testScope struct {
name string
callerType string
wantErr bool
want interface{}
}

tests := []testScope{
{
name: "empty caller",
callerType: "",
want: "",
},
{
name: "unknown caller",
callerType: "an-unknown-caller",
wantErr: true,
want: "an-unknown-caller",
},
{
name: "case-insensitive caller",
callerType: strings.ToUpper(IntelliJ),
want: IntelliJ,
},
{
name: "trimming space from caller",
callerType: fmt.Sprintf(" %s\t", VSCode),
want: VSCode,
},
}
for _, c := range []string{VSCode, IntelliJ, JBoss} {
tests = append(tests, testScope{
name: fmt.Sprintf("valid caller: %s", c),
callerType: c,
want: c,
})
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContext(context.Background())

err := SetCaller(ctx, tt.callerType)

if !tt.wantErr == (err != nil) {
t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
}

got := GetContextProperties(ctx)[Caller]
if got != tt.want {
t.Errorf("SetCaller() = %v, want %v", got, tt.want)
}
})
}
}
1 change: 1 addition & 0 deletions pkg/segment/segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const TelemetryClient = "odo"
const (
DisableTelemetryEnv = "ODO_DISABLE_TELEMETRY"
DebugTelemetryFileEnv = "ODO_DEBUG_TELEMETRY_FILE"
TelemetryCaller = "TELEMETRY_CALLER"
)

type TelemetryProperties struct {
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/cmd_dev_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ var _ = Describe("odo dev command tests", func() {
Expect(td.Properties.CmdProperties[segment.ComponentType]).To(ContainSubstring("nodejs"))
Expect(td.Properties.CmdProperties[segment.Language]).To(ContainSubstring("nodejs"))
Expect(td.Properties.CmdProperties[segment.ProjectType]).To(ContainSubstring("nodejs"))
Expect(td.Properties.CmdProperties).Should(HaveKey(segment.Caller))
Expect(td.Properties.CmdProperties[segment.Caller]).To(BeEmpty())
})
})

Expand Down
3 changes: 2 additions & 1 deletion tests/integration/cmd_devfile_deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ ComponentSettings:
Expect(td.Properties.CmdProperties[segment.Language]).To(ContainSubstring("javascript"))
Expect(td.Properties.CmdProperties[segment.ProjectType]).To(ContainSubstring("nodejs"))
Expect(td.Properties.CmdProperties[segment.Flags]).To(BeEmpty())

Expect(td.Properties.CmdProperties).Should(HaveKey(segment.Caller))
Expect(td.Properties.CmdProperties[segment.Caller]).To(BeEmpty())
})
})

Expand Down
112 changes: 91 additions & 21 deletions tests/integration/cmd_devfile_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import (
"path/filepath"

"github.com/redhat-developer/odo/pkg/odo/cli/messages"
segment "github.com/redhat-developer/odo/pkg/segment/context"
"github.com/redhat-developer/odo/pkg/segment"
segmentContext "github.com/redhat-developer/odo/pkg/segment/context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -344,26 +345,95 @@ var _ = Describe("odo devfile init command tests", func() {
Expect(metadata.Language).To(BeEquivalentTo("nodejs"))
})
})
When("recording telemetry data", func() {
BeforeEach(func() {
helper.EnableTelemetryDebug()
helper.Cmd("odo", "init", "--name", "aname", "--devfile", "go").ShouldPass().Out()
})
AfterEach(func() {
helper.ResetTelemetry()
})
It("should record the telemetry data correctly", func() {
td := helper.GetTelemetryDebugData()
Expect(td.Event).To(ContainSubstring("odo init"))
Expect(td.Properties.Success).To(BeTrue())
Expect(td.Properties.Error == "").To(BeTrue())
Expect(td.Properties.ErrorType == "").To(BeTrue())
Expect(td.Properties.CmdProperties[segment.DevfileName]).To(ContainSubstring("aname"))
Expect(td.Properties.CmdProperties[segment.ComponentType]).To(ContainSubstring("Go"))
Expect(td.Properties.CmdProperties[segment.Language]).To(ContainSubstring("Go"))
Expect(td.Properties.CmdProperties[segment.ProjectType]).To(ContainSubstring("Go"))
Expect(td.Properties.CmdProperties[segment.Flags]).To(ContainSubstring("devfile name"))

type telemetryTest struct {
title string
env map[string]string
callerChecker func(stdout, stderr string, data segment.TelemetryData)
}
allowedTelemetryCallers := []string{segmentContext.VSCode, segmentContext.IntelliJ, segmentContext.JBoss}
telemetryTests := []telemetryTest{
{
title: "no caller env var",
callerChecker: func(_, _ string, td segment.TelemetryData) {
cmdProperties := td.Properties.CmdProperties
Expect(cmdProperties).Should(HaveKey(segmentContext.Caller))
Expect(cmdProperties[segmentContext.Caller]).To(BeEmpty())
},
},
{
title: "empty caller env var",
env: map[string]string{
segment.TelemetryCaller: "",
},
callerChecker: func(_, _ string, td segment.TelemetryData) {
cmdProperties := td.Properties.CmdProperties
Expect(cmdProperties).Should(HaveKey(segmentContext.Caller))
Expect(cmdProperties[segmentContext.Caller]).To(BeEmpty())
},
},
{
title: "invalid caller env var",
env: map[string]string{
segment.TelemetryCaller: "an-invalid-caller",
},
callerChecker: func(stdout, stderr string, td segment.TelemetryData) {
By("not disclosing list of allowed values", func() {
helper.DontMatchAllInOutput(stdout, allowedTelemetryCallers)
helper.DontMatchAllInOutput(stderr, allowedTelemetryCallers)
})

By("setting the value as caller property in telemetry even if it is invalid", func() {
Expect(td.Properties.CmdProperties[segmentContext.Caller]).To(Equal("an-invalid-caller"))
})
},
},
}
for _, c := range allowedTelemetryCallers {
c := c
telemetryTests = append(telemetryTests, telemetryTest{
title: fmt.Sprintf("valid caller env var: %s", c),
env: map[string]string{
segment.TelemetryCaller: c,
},
callerChecker: func(_, _ string, td segment.TelemetryData) {
Expect(td.Properties.CmdProperties[segmentContext.Caller]).To(Equal(c))
},
})
}

for _, tt := range telemetryTests {
tt := tt
When("recording telemetry data with "+tt.title, func() {
var stdout string
var stderr string
BeforeEach(func() {
helper.EnableTelemetryDebug()
cmd := helper.Cmd("odo", "init", "--name", "aname", "--devfile", "go")
for k, v := range tt.env {
cmd = cmd.AddEnv(fmt.Sprintf("%s=%s", k, v))
}
stdout, stderr = cmd.ShouldPass().OutAndErr()
})

AfterEach(func() {
helper.ResetTelemetry()
})

It("should record the telemetry data correctly", func() {
td := helper.GetTelemetryDebugData()
Expect(td.Event).To(ContainSubstring("odo init"))
Expect(td.Properties.Success).To(BeTrue())
Expect(td.Properties.Error == "").To(BeTrue())
Expect(td.Properties.ErrorType == "").To(BeTrue())
Expect(td.Properties.CmdProperties[segmentContext.DevfileName]).To(ContainSubstring("aname"))
Expect(td.Properties.CmdProperties[segmentContext.ComponentType]).To(ContainSubstring("Go"))
Expect(td.Properties.CmdProperties[segmentContext.Language]).To(ContainSubstring("Go"))
Expect(td.Properties.CmdProperties[segmentContext.ProjectType]).To(ContainSubstring("Go"))
Expect(td.Properties.CmdProperties[segmentContext.Flags]).To(ContainSubstring("devfile name"))
tt.callerChecker(stdout, stderr, td)
})

})
})
}
})

0 comments on commit 1c9d6b3

Please sign in to comment.