Skip to content

Commit

Permalink
fix: dev server commands emit events (#410)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mike-zorn authored Aug 30, 2024
1 parent afe5281 commit 2019b77
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 106 deletions.
30 changes: 30 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}))
},
}

```
18 changes: 17 additions & 1 deletion cmd/dev_server/dev_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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(
Expand Down
9 changes: 5 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
114 changes: 13 additions & 101 deletions internal/analytics/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {}
28 changes: 28 additions & 0 deletions internal/analytics/log_client.go
Original file line number Diff line number Diff line change
@@ -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() {}
60 changes: 60 additions & 0 deletions internal/analytics/mock.go
Original file line number Diff line number Diff line change
@@ -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() {}
18 changes: 18 additions & 0 deletions internal/analytics/noop_client.go
Original file line number Diff line number Diff line change
@@ -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() {}
12 changes: 12 additions & 0 deletions internal/analytics/tracker.go
Original file line number Diff line number Diff line change
@@ -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()
}

0 comments on commit 2019b77

Please sign in to comment.