From 2019b772cab8fd585bed6b58e3625fc873e1fa84 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Fri, 30 Aug 2024 10:54:42 -0700 Subject: [PATCH] fix: dev server commands emit events (#410) This adds events for the dev server commands so that we can track adoption. Along the way, I also did the following * Added a LogClient implementation for testing event tracking * Simplified the TrackerFn so that it was easier to swap clients * Split up the event tracker implementations into multiple files * Added a note on what steps are needed to add a command because we missed 2/3 for the dev server. --- CONTRIBUTING.md | 30 ++++++++ cmd/dev_server/dev_server.go | 18 ++++- cmd/root.go | 9 +-- internal/analytics/client.go | 114 ++++-------------------------- internal/analytics/log_client.go | 28 ++++++++ internal/analytics/mock.go | 60 ++++++++++++++++ internal/analytics/noop_client.go | 18 +++++ internal/analytics/tracker.go | 12 ++++ 8 files changed, 183 insertions(+), 106 deletions(-) create mode 100644 internal/analytics/log_client.go create mode 100644 internal/analytics/mock.go create mode 100644 internal/analytics/noop_client.go create mode 100644 internal/analytics/tracker.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 62ee3614..c2e1dad3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,3 +16,33 @@ To install the repo's git hooks, run `make install-hooks`. The pre-commit hook checks that relevant project files are formatted with `go fmt`, and that the `go.mod/go.sum` files are tidy. + +## Adding a new command + +There are a few things you need to do in order to wire up a new top-level command. + +1. Add your command to the root command by calling `cmd.AddComand` in the `NewRootCommand` method of the `cmd` package. +2. Update the root command's usage template by modifying the `getUsageTemplate` method in the `cmd` package. +3. Instrument your command by setting a `PreRun` or `PersistentPreRun` on your command which calls `tracker.SendCommandRunEvent`. Example below. +```go +cmd := &cobra.Command{ + Use: "dev-server", + Short: "Development server", + Long: "Start and use a local development server for overriding flag values.", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + + tracker := analyticsTrackerFn( + viper.GetString(cliflags.AccessTokenFlag), + viper.GetString(cliflags.BaseURIFlag), + viper.GetBool(cliflags.AnalyticsOptOut), + ) + tracker.SendCommandRunEvent(cmdAnalytics.CmdRunEventProperties( + cmd, + "dev-server", + map[string]interface{}{ + "action": cmd.Name(), + })) + }, +} + +``` diff --git a/cmd/dev_server/dev_server.go b/cmd/dev_server/dev_server.go index 947fdd2c..f04b0417 100644 --- a/cmd/dev_server/dev_server.go +++ b/cmd/dev_server/dev_server.go @@ -3,6 +3,8 @@ package dev_server import ( "fmt" + cmdAnalytics "github.com/launchdarkly/ldcli/cmd/analytics" + "github.com/launchdarkly/ldcli/internal/analytics" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -12,11 +14,25 @@ import ( "github.com/launchdarkly/ldcli/internal/resources" ) -func NewDevServerCmd(client resources.Client, ldClient dev_server.Client) *cobra.Command { +func NewDevServerCmd(client resources.Client, analyticsTrackerFn analytics.TrackerFn, ldClient dev_server.Client) *cobra.Command { cmd := &cobra.Command{ Use: "dev-server", Short: "Development server", Long: "Start and use a local development server for overriding flag values.", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + + tracker := analyticsTrackerFn( + viper.GetString(cliflags.AccessTokenFlag), + viper.GetString(cliflags.BaseURIFlag), + viper.GetBool(cliflags.AnalyticsOptOut), + ) + tracker.SendCommandRunEvent(cmdAnalytics.CmdRunEventProperties( + cmd, + "dev-server", + map[string]interface{}{ + "action": cmd.Name(), + })) + }, } cmd.PersistentFlags().String( diff --git a/cmd/root.go b/cmd/root.go index fbc36290..4f95e4a3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -193,7 +193,7 @@ func NewRootCommand( cmd.AddCommand(NewQuickStartCmd(analyticsTrackerFn, clients.EnvironmentsClient, clients.FlagsClient)) cmd.AddCommand(logincmd.NewLoginCmd(resources.NewClient(version))) cmd.AddCommand(resourcecmd.NewResourcesCmd()) - cmd.AddCommand(devcmd.NewDevServerCmd(resources.NewClient(version), dev_server.NewClient(version))) + cmd.AddCommand(devcmd.NewDevServerCmd(resources.NewClient(version), analyticsTrackerFn, dev_server.NewClient(version))) resourcecmd.AddAllResourceCmds(cmd, clients.ResourcesClient, analyticsTrackerFn) // add non-generated commands @@ -224,11 +224,12 @@ func Execute(version string) { } configService := config.NewService(resources.NewClient(version)) trackerFn := analytics.ClientFn{ - ID: uuid.New().String(), + ID: uuid.New().String(), + Version: version, } rootCmd, err := NewRootCommand( configService, - trackerFn.Tracker(version), + trackerFn.Tracker, clients, version, true, @@ -266,7 +267,7 @@ See each command's help for details on how to use the generated script.`, rootCm outcome = analytics.SUCCESS } - analyticsClient := trackerFn.Tracker(version)( + analyticsClient := trackerFn.Tracker( viper.GetString(cliflags.AccessTokenFlag), viper.GetString(cliflags.BaseURIFlag), viper.GetBool(cliflags.AnalyticsOptOut), diff --git a/internal/analytics/client.go b/internal/analytics/client.go index 9104306e..938882b1 100644 --- a/internal/analytics/client.go +++ b/internal/analytics/client.go @@ -9,49 +9,27 @@ import ( "net/url" "sync" "time" - - "github.com/stretchr/testify/mock" ) -type TrackerFn func(accessToken string, baseURI string, optOut bool) Tracker - type ClientFn struct { - ID string -} - -func (fn ClientFn) Tracker(version string) TrackerFn { - return func(accessToken string, baseURI string, optOut bool) Tracker { - if optOut { - return &NoopClient{} - } - - return &Client{ - httpClient: &http.Client{ - Timeout: time.Second * 3, - }, - id: fn.ID, - version: version, - accessToken: accessToken, - baseURI: baseURI, - } - } + ID string + Version string } -type NoopClientFn struct{} - -func (fn NoopClientFn) Tracker() TrackerFn { - return func(_ string, _ string, _ bool) Tracker { +func (fn ClientFn) Tracker(accessToken string, baseURI string, optOut bool) Tracker { + if optOut { return &NoopClient{} } -} -type Tracker interface { - SendCommandRunEvent(properties map[string]interface{}) - SendCommandCompletedEvent(outcome string) - SendSetupStepStartedEvent(step string) - SendSetupSDKSelectedEvent(sdk string) - SendSetupFlagToggledEvent(on bool, count int, duration_ms int64) - Wait() + return &Client{ + httpClient: &http.Client{ + Timeout: time.Second * 3, + }, + id: fn.ID, + version: fn.Version, + accessToken: accessToken, + baseURI: baseURI, + } } type Client struct { @@ -164,69 +142,3 @@ func (c *Client) SendSetupFlagToggledEvent(on bool, count int, duration_ms int64 func (a *Client) Wait() { a.wg.Wait() } - -type NoopClient struct{} - -func (c *NoopClient) SendCommandRunEvent(properties map[string]interface{}) {} -func (c *NoopClient) SendCommandCompletedEvent(outcome string) {} -func (c *NoopClient) SendSetupStepStartedEvent(step string) {} -func (c *NoopClient) SendSetupSDKSelectedEvent(sdk string) {} -func (c *NoopClient) SendSetupFlagToggledEvent(on bool, count int, duration_ms int64) {} -func (a *NoopClient) Wait() {} - -type MockTracker struct { - mock.Mock - ID string -} - -func (m *MockTracker) sendEvent(eventName string, properties map[string]interface{}) { - properties["id"] = m.ID - m.Called(eventName, properties) -} - -func (m *MockTracker) SendCommandRunEvent(properties map[string]interface{}) { - m.sendEvent( - "CLI Command Run", - properties, - ) -} - -func (m *MockTracker) SendCommandCompletedEvent(outcome string) { - m.sendEvent( - "CLI Command Completed", - map[string]interface{}{ - "outcome": outcome, - }, - ) -} - -func (m *MockTracker) SendSetupStepStartedEvent(step string) { - m.sendEvent( - "CLI Setup Step Started", - map[string]interface{}{ - "step": step, - }, - ) -} - -func (m *MockTracker) SendSetupSDKSelectedEvent(sdk string) { - m.sendEvent( - "CLI Setup SDK Selected", - map[string]interface{}{ - "sdk": sdk, - }, - ) -} - -func (m *MockTracker) SendSetupFlagToggledEvent(on bool, count int, duration_ms int64) { - m.sendEvent( - "CLI Setup Flag Toggled", - map[string]interface{}{ - "on": on, - "count": count, - "duration_ms": duration_ms, - }, - ) -} - -func (a *MockTracker) Wait() {} diff --git a/internal/analytics/log_client.go b/internal/analytics/log_client.go new file mode 100644 index 00000000..ae99ca48 --- /dev/null +++ b/internal/analytics/log_client.go @@ -0,0 +1,28 @@ +package analytics + +import "log" + +type LogClientFn struct{} + +func (fn LogClientFn) Tracker(_ string, _ string, _ bool) Tracker { + return &LogClient{} +} + +type LogClient struct{} + +func (c *LogClient) SendCommandRunEvent(properties map[string]interface{}) { + log.Printf("SendCommandRunEvent, properties: %v", properties) +} +func (c *LogClient) SendCommandCompletedEvent(outcome string) { + log.Printf("SendCommandCompletedEvent, outcome: %v", outcome) +} +func (c *LogClient) SendSetupStepStartedEvent(step string) { + log.Printf("SendSetupStepStartedEvent, step: %v", step) +} +func (c *LogClient) SendSetupSDKSelectedEvent(sdk string) { + log.Printf("SendSetupSDKSelectedEvent, sdk: %v", sdk) +} +func (c *LogClient) SendSetupFlagToggledEvent(on bool, count int, duration_ms int64) { + log.Printf("SendSetupFlagToggledEvent, count: %v", count) +} +func (a *LogClient) Wait() {} diff --git a/internal/analytics/mock.go b/internal/analytics/mock.go new file mode 100644 index 00000000..88e8870f --- /dev/null +++ b/internal/analytics/mock.go @@ -0,0 +1,60 @@ +package analytics + +import "github.com/stretchr/testify/mock" + +type MockTracker struct { + mock.Mock + ID string +} + +func (m *MockTracker) sendEvent(eventName string, properties map[string]interface{}) { + properties["id"] = m.ID + m.Called(eventName, properties) +} + +func (m *MockTracker) SendCommandRunEvent(properties map[string]interface{}) { + m.sendEvent( + "CLI Command Run", + properties, + ) +} + +func (m *MockTracker) SendCommandCompletedEvent(outcome string) { + m.sendEvent( + "CLI Command Completed", + map[string]interface{}{ + "outcome": outcome, + }, + ) +} + +func (m *MockTracker) SendSetupStepStartedEvent(step string) { + m.sendEvent( + "CLI Setup Step Started", + map[string]interface{}{ + "step": step, + }, + ) +} + +func (m *MockTracker) SendSetupSDKSelectedEvent(sdk string) { + m.sendEvent( + "CLI Setup SDK Selected", + map[string]interface{}{ + "sdk": sdk, + }, + ) +} + +func (m *MockTracker) SendSetupFlagToggledEvent(on bool, count int, duration_ms int64) { + m.sendEvent( + "CLI Setup Flag Toggled", + map[string]interface{}{ + "on": on, + "count": count, + "duration_ms": duration_ms, + }, + ) +} + +func (a *MockTracker) Wait() {} diff --git a/internal/analytics/noop_client.go b/internal/analytics/noop_client.go new file mode 100644 index 00000000..d304e09b --- /dev/null +++ b/internal/analytics/noop_client.go @@ -0,0 +1,18 @@ +package analytics + +type NoopClientFn struct{} + +func (fn NoopClientFn) Tracker() TrackerFn { + return func(_ string, _ string, _ bool) Tracker { + return &NoopClient{} + } +} + +type NoopClient struct{} + +func (c *NoopClient) SendCommandRunEvent(properties map[string]interface{}) {} +func (c *NoopClient) SendCommandCompletedEvent(outcome string) {} +func (c *NoopClient) SendSetupStepStartedEvent(step string) {} +func (c *NoopClient) SendSetupSDKSelectedEvent(sdk string) {} +func (c *NoopClient) SendSetupFlagToggledEvent(on bool, count int, duration_ms int64) {} +func (a *NoopClient) Wait() {} diff --git a/internal/analytics/tracker.go b/internal/analytics/tracker.go new file mode 100644 index 00000000..f54bc4fd --- /dev/null +++ b/internal/analytics/tracker.go @@ -0,0 +1,12 @@ +package analytics + +type TrackerFn func(accessToken string, baseURI string, optOut bool) Tracker + +type Tracker interface { + SendCommandRunEvent(properties map[string]interface{}) + SendCommandCompletedEvent(outcome string) + SendSetupStepStartedEvent(step string) + SendSetupSDKSelectedEvent(sdk string) + SendSetupFlagToggledEvent(on bool, count int, duration_ms int64) + Wait() +}