Skip to content

Commit a79c953

Browse files
rm3lfeloy
andauthored
Track preference options usage (#6843)
* Add integration test highlighting the expectations * Record parameter (and value) set or unset using the 'odo preference set/unset' commands By default, values are anonymized. Only parameters explicitly declared list will be recorded verbatim. This list is currently empty, but that might change later on. * Mark all current preference parameters except ImageRegistry as clear-text Co-authored-by: Philippe Martin <[email protected]> --------- Co-authored-by: Philippe Martin <[email protected]>
1 parent 78c9bed commit a79c953

File tree

5 files changed

+197
-41
lines changed

5 files changed

+197
-41
lines changed

pkg/odo/cli/preference/set.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/redhat-developer/odo/pkg/log"
99
"github.com/redhat-developer/odo/pkg/odo/cmdline"
1010
"github.com/redhat-developer/odo/pkg/odo/util"
11+
scontext "github.com/redhat-developer/odo/pkg/segment/context"
1112

1213
"github.com/redhat-developer/odo/pkg/odo/cli/ui"
1314
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
@@ -83,6 +84,8 @@ func (o *SetOptions) Run(ctx context.Context) (err error) {
8384
}
8485

8586
log.Successf("Value of '%s' preference was set to '%s'", o.paramName, o.paramValue)
87+
88+
scontext.SetPreferenceParameter(ctx, o.paramName, &o.paramValue)
8689
return nil
8790
}
8891

pkg/odo/cli/preference/unset.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/redhat-developer/odo/pkg/odo/cmdline"
1111
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
1212
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
13+
scontext "github.com/redhat-developer/odo/pkg/segment/context"
1314

1415
"github.com/spf13/cobra"
1516
ktemplates "k8s.io/kubectl/pkg/util/templates"
@@ -83,6 +84,8 @@ func (o *UnsetOptions) Run(ctx context.Context) (err error) {
8384
}
8485

8586
log.Successf("Value of '%s' preference was removed from preferences. Its default value will be used.", o.paramName)
87+
88+
scontext.SetPreferenceParameter(ctx, o.paramName, nil)
8689
return nil
8790

8891
}

pkg/segment/context/context.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package context
33
import (
44
"context"
55
"fmt"
6+
"hash/adler32"
67
"os"
8+
"strconv"
79
"strings"
810
"sync"
911
"time"
@@ -34,6 +36,8 @@ const (
3436
Flags = "flags"
3537
Platform = "platform"
3638
PlatformVersion = "platformVersion"
39+
PreferenceParameter = "parameter"
40+
PreferenceValue = "value"
3741
)
3842

3943
const (
@@ -42,6 +46,16 @@ const (
4246
JBoss = "jboss"
4347
)
4448

49+
// Add the (case-insensitive) preference parameter name here to have the corresponding value sent verbatim to telemetry.
50+
var clearTextPreferenceParams = []string{
51+
"ConsentTelemetry",
52+
"Ephemeral",
53+
"PushTimeout",
54+
"RegistryCacheTime",
55+
"Timeout",
56+
"UpdateNotification",
57+
}
58+
4559
type contextKey struct{}
4660

4761
var key = contextKey{}
@@ -228,6 +242,34 @@ func SetCaller(ctx context.Context, caller string) error {
228242
return err
229243
}
230244

245+
// SetPreferenceParameter tracks the preferences options usage, by recording both the parameter name and value.
246+
// By default, values are anonymized. Only parameters explicitly declared in the 'clearTextPreferenceParams' list will be recorded verbatim.
247+
// Setting value to nil means that the parameter has been unset in the preferences; so the value will not be recorded.
248+
func SetPreferenceParameter(ctx context.Context, param string, value *string) {
249+
setContextProperty(ctx, PreferenceParameter, param)
250+
251+
if value == nil {
252+
return
253+
}
254+
255+
isClearTextParam := func() bool {
256+
for _, clearTextParam := range clearTextPreferenceParams {
257+
if strings.EqualFold(param, clearTextParam) {
258+
return true
259+
}
260+
}
261+
return false
262+
}
263+
264+
recordedValue := *value
265+
if !isClearTextParam() {
266+
// adler32 for fast (and short) checksum computation, while minimizing the probability of collisions (which are not that important here).
267+
// We just want to make sure that the same value returns the same anonymized string, while making it hard to guess the original string.
268+
recordedValue = strconv.FormatUint(uint64(adler32.Checksum([]byte(recordedValue))), 16)
269+
}
270+
setContextProperty(ctx, PreferenceValue, recordedValue)
271+
}
272+
231273
// GetPreviousTelemetryStatus gets the telemetry status that was seen before a command is run
232274
func GetPreviousTelemetryStatus(ctx context.Context) bool {
233275
wasEnabled, ok := GetContextProperties(ctx)[PreviousTelemetryStatus]

tests/helper/helper_telemetry.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ func GetDebugTelemetryFile() string {
4747
return os.Getenv(DebugTelemetryFileEnv)
4848
}
4949

50+
func ClearTelemetryFile() {
51+
err := os.Truncate(GetDebugTelemetryFile(), 0)
52+
Expect(err).ShouldNot(HaveOccurred())
53+
}
54+
5055
// GetTelemetryDebugData gets telemetry data dumped into temp file for testing/debugging
5156
func GetTelemetryDebugData() segment.TelemetryData {
5257
var data []byte

tests/integration/cmd_pref_config_test.go

Lines changed: 144 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strings"
78

89
. "github.com/onsi/ginkgo/v2"
910
. "github.com/onsi/gomega"
@@ -204,64 +205,166 @@ OdoSettings:
204205
})
205206
})
206207

207-
When("Telemetry is enabled in preferences", func() {
208+
When("telemetry is enabled", func() {
209+
var prefClient preference.Client
210+
208211
BeforeEach(func() {
209-
prefClient := helper.EnableTelemetryDebug()
210-
err := prefClient.SetConfiguration(preference.ConsentTelemetrySetting, "true")
211-
Expect(err).ShouldNot(HaveOccurred())
212-
Expect(os.Unsetenv(segment.TrackingConsentEnv)).NotTo(HaveOccurred())
212+
prefClient = helper.EnableTelemetryDebug()
213213
})
214214

215215
AfterEach(func() {
216216
helper.ResetTelemetry()
217217
})
218218

219-
When("setting ConsentTelemetry to false", func() {
220-
BeforeEach(func() {
221-
helper.Cmd("odo", "preference", "set", "ConsentTelemetry", "false", "--force").ShouldPass()
219+
for _, tt := range []struct {
220+
prefParam string
221+
value string
222+
differentValueIfAnonymized string
223+
clearText bool
224+
}{
225+
{"ConsentTelemetry", "true", "", true},
226+
{"Ephemeral", "false", "", true},
227+
{"UpdateNotification", "true", "", true},
228+
{"PushTimeout", "1m", "", true},
229+
{"RegistryCacheTime", "2s", "", true},
230+
{"Timeout", "30s", "", true},
231+
{"ImageRegistry", "quay.io/some-org", "ghcr.io/my-org", false},
232+
} {
233+
tt := tt
234+
form := "hashed"
235+
if tt.clearText {
236+
form = "plain"
237+
}
238+
239+
When("unsetting value for preference "+tt.prefParam, func() {
240+
BeforeEach(func() {
241+
helper.Cmd("odo", "preference", "unset", tt.prefParam, "--force").ShouldPass()
242+
})
243+
244+
It("should track parameter that is unset without any value", func() {
245+
helper.Cmd("odo", "preference", "unset", tt.prefParam, "--force").ShouldPass()
246+
td := helper.GetTelemetryDebugData()
247+
Expect(td.Event).To(ContainSubstring("odo preference unset"))
248+
Expect(td.Properties.Success).To(BeTrue())
249+
Expect(td.Properties.Error).To(BeEmpty())
250+
Expect(td.Properties.ErrorType).To(BeEmpty())
251+
Expect(td.Properties.CmdProperties[segmentContext.Flags]).To(Equal("force"))
252+
Expect(td.Properties.CmdProperties[segmentContext.PreferenceParameter]).To(Equal(strings.ToLower(tt.prefParam)))
253+
valueRecorded, present := td.Properties.CmdProperties[segmentContext.PreferenceValue]
254+
Expect(present).To(BeFalse(), fmt.Sprintf("no value should be recorded for preference %q, fot %q", tt.prefParam, valueRecorded))
255+
})
222256
})
223257

224-
// https://github.com/redhat-developer/odo/issues/6790
225-
It("should record the odo-preference-set command in telemetry", func() {
226-
td := helper.GetTelemetryDebugData()
227-
Expect(td.Event).To(ContainSubstring("odo preference set"))
228-
Expect(td.Properties.Success).To(BeTrue())
229-
Expect(td.Properties.Error).To(BeEmpty())
230-
Expect(td.Properties.ErrorType).To(BeEmpty())
231-
Expect(td.Properties.CmdProperties[segmentContext.Flags]).To(Equal("force"))
232-
Expect(td.Properties.CmdProperties[segmentContext.PreviousTelemetryStatus]).To(BeTrue())
233-
Expect(td.Properties.CmdProperties[segmentContext.TelemetryStatus]).To(BeFalse())
258+
When("setting value for preference "+tt.prefParam, func() {
259+
BeforeEach(func() {
260+
if !tt.clearText {
261+
Expect(tt.differentValueIfAnonymized).ShouldNot(Equal(tt.value),
262+
"test not written as expected. Values should be different for preference parameters declared as anonymized.")
263+
}
264+
helper.Cmd("odo", "preference", "set", tt.prefParam, tt.value, "--force").ShouldPass()
265+
})
266+
267+
It(fmt.Sprintf("should track parameter that is set along with its %s value", form), func() {
268+
td := helper.GetTelemetryDebugData()
269+
Expect(td.Event).To(ContainSubstring("odo preference set"))
270+
Expect(td.Properties.Success).To(BeTrue())
271+
Expect(td.Properties.Error).To(BeEmpty())
272+
Expect(td.Properties.ErrorType).To(BeEmpty())
273+
Expect(td.Properties.CmdProperties[segmentContext.Flags]).To(Equal("force"))
274+
Expect(td.Properties.CmdProperties[segmentContext.PreferenceParameter]).To(Equal(strings.ToLower(tt.prefParam)))
275+
Expect(td.Properties.CmdProperties[segmentContext.PreferenceValue]).ShouldNot(BeEmpty())
276+
if tt.clearText {
277+
Expect(td.Properties.CmdProperties[segmentContext.PreferenceValue]).Should(Equal(tt.value))
278+
} else {
279+
Expect(td.Properties.CmdProperties[segmentContext.PreferenceValue]).ShouldNot(Equal(tt.value))
280+
}
281+
})
282+
283+
if !tt.clearText {
284+
It("should anonymize values set such that same strings have same hash", func() {
285+
td := helper.GetTelemetryDebugData()
286+
Expect(td.Properties.CmdProperties[segmentContext.PreferenceValue]).ShouldNot(BeEmpty())
287+
pref1Val, ok := td.Properties.CmdProperties[segmentContext.PreferenceValue].(string)
288+
Expect(ok).To(BeTrue(), fmt.Sprintf("value recorded in telemetry for preference %q is expected to be a string", tt.prefParam))
289+
290+
helper.ClearTelemetryFile()
291+
292+
helper.Cmd("odo", "preference", "set", tt.prefParam, tt.value, "--force").ShouldPass()
293+
td = helper.GetTelemetryDebugData()
294+
Expect(td.Properties.CmdProperties[segmentContext.PreferenceValue]).ShouldNot(BeEmpty())
295+
pref2Val, ok := td.Properties.CmdProperties[segmentContext.PreferenceValue].(string)
296+
Expect(ok).To(BeTrue(), fmt.Sprintf("value recorded in telemetry for preference %q is expected to be a string", tt.prefParam))
297+
298+
Expect(pref1Val).To(Equal(pref2Val))
299+
})
300+
301+
It("should anonymize values set such that different strings will not have same hash", func() {
302+
td := helper.GetTelemetryDebugData()
303+
Expect(td.Properties.CmdProperties[segmentContext.PreferenceValue]).ShouldNot(BeEmpty())
304+
pref1Val, ok := td.Properties.CmdProperties[segmentContext.PreferenceValue].(string)
305+
Expect(ok).To(BeTrue(), fmt.Sprintf("value recorded in telemetry for preference %q is expected to be a string", tt.prefParam))
306+
307+
helper.ClearTelemetryFile()
308+
309+
helper.Cmd("odo", "preference", "set", tt.prefParam, tt.differentValueIfAnonymized, "--force").ShouldPass()
310+
td = helper.GetTelemetryDebugData()
311+
Expect(td.Properties.CmdProperties[segmentContext.PreferenceValue]).ShouldNot(BeEmpty())
312+
pref2Val, ok := td.Properties.CmdProperties[segmentContext.PreferenceValue].(string)
313+
Expect(ok).To(BeTrue(), fmt.Sprintf("value recorded in telemetry for preference %q is expected to be a string", tt.prefParam))
314+
315+
Expect(pref1Val).ToNot(Equal(pref2Val))
316+
})
317+
}
234318
})
235-
})
236-
})
319+
}
237320

238-
When("Telemetry is disabled in preferences", func() {
239-
BeforeEach(func() {
240-
prefClient := helper.EnableTelemetryDebug()
241-
err := prefClient.SetConfiguration(preference.ConsentTelemetrySetting, "false")
242-
Expect(err).ShouldNot(HaveOccurred())
243-
Expect(os.Unsetenv(segment.TrackingConsentEnv)).NotTo(HaveOccurred())
244-
})
321+
When("telemetry is enabled in preferences", func() {
322+
BeforeEach(func() {
323+
Expect(os.Unsetenv(segment.TrackingConsentEnv)).NotTo(HaveOccurred())
324+
Expect(prefClient.SetConfiguration(preference.ConsentTelemetrySetting, "true")).ShouldNot(HaveOccurred())
325+
})
245326

246-
AfterEach(func() {
247-
helper.ResetTelemetry()
327+
When("setting ConsentTelemetry to false", func() {
328+
BeforeEach(func() {
329+
helper.Cmd("odo", "preference", "set", "ConsentTelemetry", "false", "--force").ShouldPass()
330+
})
331+
332+
// https://github.com/redhat-developer/odo/issues/6790
333+
It("should record the odo-preference-set command in telemetry", func() {
334+
td := helper.GetTelemetryDebugData()
335+
Expect(td.Event).To(ContainSubstring("odo preference set"))
336+
Expect(td.Properties.Success).To(BeTrue())
337+
Expect(td.Properties.Error).To(BeEmpty())
338+
Expect(td.Properties.ErrorType).To(BeEmpty())
339+
Expect(td.Properties.CmdProperties[segmentContext.Flags]).To(Equal("force"))
340+
Expect(td.Properties.CmdProperties[segmentContext.PreviousTelemetryStatus]).To(BeTrue())
341+
Expect(td.Properties.CmdProperties[segmentContext.TelemetryStatus]).To(BeFalse())
342+
})
343+
})
248344
})
249345

250-
When("setting ConsentTelemetry to true", func() {
346+
When("Telemetry is disabled in preferences", func() {
251347
BeforeEach(func() {
252-
helper.Cmd("odo", "preference", "set", "ConsentTelemetry", "true", "--force").ShouldPass()
348+
Expect(os.Unsetenv(segment.TrackingConsentEnv)).NotTo(HaveOccurred())
349+
Expect(prefClient.SetConfiguration(preference.ConsentTelemetrySetting, "false")).ShouldNot(HaveOccurred())
253350
})
254351

255-
// https://github.com/redhat-developer/odo/issues/6790
256-
It("should record the odo-preference-set command in telemetry", func() {
257-
td := helper.GetTelemetryDebugData()
258-
Expect(td.Event).To(ContainSubstring("odo preference set"))
259-
Expect(td.Properties.Success).To(BeTrue())
260-
Expect(td.Properties.Error).To(BeEmpty())
261-
Expect(td.Properties.ErrorType).To(BeEmpty())
262-
Expect(td.Properties.CmdProperties[segmentContext.Flags]).To(Equal("force"))
263-
Expect(td.Properties.CmdProperties[segmentContext.PreviousTelemetryStatus]).To(BeFalse())
264-
Expect(td.Properties.CmdProperties[segmentContext.TelemetryStatus]).To(BeTrue())
352+
When("setting ConsentTelemetry to true", func() {
353+
BeforeEach(func() {
354+
helper.Cmd("odo", "preference", "set", "ConsentTelemetry", "true", "--force").ShouldPass()
355+
})
356+
357+
// https://github.com/redhat-developer/odo/issues/6790
358+
It("should record the odo-preference-set command in telemetry", func() {
359+
td := helper.GetTelemetryDebugData()
360+
Expect(td.Event).To(ContainSubstring("odo preference set"))
361+
Expect(td.Properties.Success).To(BeTrue())
362+
Expect(td.Properties.Error).To(BeEmpty())
363+
Expect(td.Properties.ErrorType).To(BeEmpty())
364+
Expect(td.Properties.CmdProperties[segmentContext.Flags]).To(Equal("force"))
365+
Expect(td.Properties.CmdProperties[segmentContext.PreviousTelemetryStatus]).To(BeFalse())
366+
Expect(td.Properties.CmdProperties[segmentContext.TelemetryStatus]).To(BeTrue())
367+
})
265368
})
266369
})
267370
})

0 commit comments

Comments
 (0)