diff --git a/cmd/quickstart.go b/cmd/quickstart.go index 51a38f67..9e3dd2ef 100644 --- a/cmd/quickstart.go +++ b/cmd/quickstart.go @@ -11,25 +11,28 @@ import ( "ldcli/cmd/cliflags" "ldcli/cmd/validators" + "ldcli/internal/analytics" "ldcli/internal/environments" "ldcli/internal/flags" "ldcli/internal/quickstart" ) func NewQuickStartCmd( + analyticsTracker analytics.Tracker, environmentsClient environments.Client, flagsClient flags.Client, ) *cobra.Command { return &cobra.Command{ Args: validators.Validate(), Long: "", - RunE: runQuickStart(environmentsClient, flagsClient), + RunE: runQuickStart(analyticsTracker, environmentsClient, flagsClient), Short: "Setup guide to create your first feature flag", Use: "setup", } } func runQuickStart( + analyticsTracker analytics.Tracker, environmentsClient environments.Client, flagsClient flags.Client, ) func(*cobra.Command, []string) error { @@ -42,6 +45,7 @@ func runQuickStart( defer f.Close() _, err = tea.NewProgram(quickstart.NewContainerModel( + analyticsTracker, environmentsClient, flagsClient, viper.GetString(cliflags.AccessTokenFlag), diff --git a/cmd/root.go b/cmd/root.go index 8339801a..780bb01f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -142,7 +142,7 @@ func NewRootCommand( cmd.AddCommand(flagsCmd) cmd.AddCommand(membersCmd) cmd.AddCommand(projectsCmd) - cmd.AddCommand(NewQuickStartCmd(clients.EnvironmentsClient, clients.FlagsClient)) + cmd.AddCommand(NewQuickStartCmd(analyticsTracker, clients.EnvironmentsClient, clients.FlagsClient)) addAllResourceCmds(cmd, clients.GenericClient) diff --git a/internal/analytics/client.go b/internal/analytics/client.go index 0bce52c3..51a4e45b 100644 --- a/internal/analytics/client.go +++ b/internal/analytics/client.go @@ -24,6 +24,26 @@ type Tracker interface { optOut bool, outcome string, ) + SendSetupStartedEvent( + accessToken, + baseURI string, + optOut bool, + step string, + ) + SendSetupSDKSelectedEvent( + accessToken, + baseURI string, + optOut bool, + sdk string, + ) + SendSetupFlagToggledEvent( + accessToken, + baseURI string, + optOut bool, + on bool, + count int, + duration_ms int64, + ) } type Client struct { @@ -128,6 +148,61 @@ func (c *Client) SendCommandCompletedEvent( } } +func (c *Client) SendSetupStartedEvent( + accessToken, + baseURI string, + optOut bool, + step string, +) { + c.sendEvent( + accessToken, + baseURI, + optOut, + "CLI Setup Started", + map[string]interface{}{ + "step": step, + }, + ) +} + +func (c *Client) SendSetupSDKSelectedEvent( + accessToken, + baseURI string, + optOut bool, + sdk string, +) { + c.sendEvent( + accessToken, + baseURI, + optOut, + "CLI Setup SDK Selected", + map[string]interface{}{ + "sdk": sdk, + }, + ) +} + +func (c *Client) SendSetupFlagToggledEvent( + accessToken, + baseURI string, + optOut bool, + on bool, + count int, + duration_ms int64, +) { + c.sendEvent( + accessToken, + baseURI, + optOut, + "CLI Setup Flag Toggled", + map[string]interface{}{ + "on": on, + "count": count, + "duration_ms": duration_ms, + }, + ) +} + func (a *Client) Wait() { a.wg.Wait() } @@ -150,6 +225,32 @@ func (c *NoopClient) SendCommandCompletedEvent( ) { } +func (c *NoopClient) SendSetupStartedEvent( + accessToken, + baseURI string, + optOut bool, + step string, +) { +} + +func (c *NoopClient) SendSetupSDKSelectedEvent( + accessToken, + baseURI string, + optOut bool, + sdk string, +) { +} + +func (c *NoopClient) SendSetupFlagToggledEvent( + accessToken, + baseURI string, + optOut bool, + on bool, + count int, + duration_ms int64, +) { +} + type MockTracker struct { mock.Mock ID string @@ -197,3 +298,58 @@ func (m *MockTracker) SendCommandCompletedEvent( }, ) } + +func (m *MockTracker) SendSetupStartedEvent( + accessToken, + baseURI string, + optOut bool, + step string, +) { + m.sendEvent( + accessToken, + baseURI, + optOut, + "CLI Setup Started", + map[string]interface{}{ + "step": step, + }, + ) +} + +func (m *MockTracker) SendSetupSDKSelectedEvent( + accessToken, + baseURI string, + optOut bool, + sdk string, +) { + m.sendEvent( + accessToken, + baseURI, + optOut, + "CLI Setup SDK Selected", + map[string]interface{}{ + "sdk": sdk, + }, + ) +} + +func (m *MockTracker) SendSetupFlagToggledEvent( + accessToken, + baseURI string, + optOut bool, + on bool, + count int, + duration_ms int64, +) { + m.sendEvent( + accessToken, + baseURI, + optOut, + "CLI Setup Flag Toggled", + map[string]interface{}{ + "on": on, + "count": count, + "duration_ms": duration_ms, + }, + ) +} diff --git a/internal/quickstart/choose_sdk.go b/internal/quickstart/choose_sdk.go index 303f1907..89b8b3c4 100644 --- a/internal/quickstart/choose_sdk.go +++ b/internal/quickstart/choose_sdk.go @@ -3,6 +3,8 @@ package quickstart import ( "fmt" "io" + "ldcli/cmd/cliflags" + "ldcli/internal/analytics" "strings" "github.com/charmbracelet/bubbles/help" @@ -10,6 +12,7 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/spf13/viper" ) var ( @@ -24,14 +27,17 @@ const ( ) type chooseSDKModel struct { - help help.Model - helpKeys keyMap - list list.Model - selectedIndex int - selectedSDK sdkDetail + accessToken string + analyticsTracker analytics.Tracker + baseUri string + help help.Model + helpKeys keyMap + list list.Model + selectedIndex int + selectedSDK sdkDetail } -func NewChooseSDKModel(selectedIndex int) tea.Model { +func NewChooseSDKModel(analyticsTracker analytics.Tracker, accessToken, baseUri string, selectedIndex int) tea.Model { l := list.New(sdksToItems(), sdkDelegate{}, 30, 9) l.Title = "Select your SDK:\n" // reset title styles @@ -43,7 +49,10 @@ func NewChooseSDKModel(selectedIndex int) tea.Model { l.SetFilteringEnabled(false) // TODO: try to get filtering working return chooseSDKModel{ - help: help.New(), + analyticsTracker: analyticsTracker, + accessToken: accessToken, + baseUri: baseUri, + help: help.New(), helpKeys: keyMap{ Back: BindingBack, CursorUp: BindingCursorUp, @@ -64,6 +73,7 @@ func NewChooseSDKModel(selectedIndex int) tea.Model { // Init sends commands when the model is created that will: // * select an SDK if it's already been selected func (m chooseSDKModel) Init() tea.Cmd { + m.analyticsTracker.SendSetupStartedEvent(m.accessToken, m.baseUri, viper.GetBool(cliflags.AnalyticsOptOut), "2 - sdk selection") return selectedSDK(m.selectedIndex) } diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index 9562bc50..996b5953 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -3,12 +3,14 @@ package quickstart import ( "fmt" "log" + "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/muesli/reflow/wordwrap" + "ldcli/internal/analytics" "ldcli/internal/environments" "ldcli/internal/flags" ) @@ -30,6 +32,7 @@ const ( // represents a step in the quick-start flow. type ContainerModel struct { accessToken string + analyticsTracker analytics.Tracker baseUri string currentModel tea.Model currentStep int @@ -41,29 +44,34 @@ type ContainerModel struct { gettingStarted bool quitting bool sdk sdkDetail + startTime time.Time totalSteps int width int } func NewContainerModel( + analyticsTracker analytics.Tracker, environmentsClient environments.Client, flagsClient flags.Client, accessToken string, baseUri string, ) tea.Model { return ContainerModel{ + analyticsTracker: analyticsTracker, accessToken: accessToken, baseUri: baseUri, - currentModel: NewCreateFlagModel(flagsClient, accessToken, baseUri), + currentModel: NewCreateFlagModel(analyticsTracker, flagsClient, accessToken, baseUri), currentStep: 1, environmentsClient: environmentsClient, flagsClient: flagsClient, gettingStarted: true, + startTime: time.Now(), totalSteps: 4, } } func (m ContainerModel) Init() tea.Cmd { + m.currentModel.Init() return nil } @@ -85,14 +93,16 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.currentModel, cmd = m.currentModel.Update(msg) case stepChooseSDK: m.currentStep -= 1 - m.currentModel = NewCreateFlagModel(m.flagsClient, m.accessToken, m.baseUri) + m.currentModel = NewCreateFlagModel(m.analyticsTracker, m.flagsClient, m.accessToken, m.baseUri) + cmd = m.currentModel.Init() case stepShowSDKInstructions: m.currentStep -= 1 - m.currentModel = NewChooseSDKModel(m.sdk.index) + m.currentModel = NewChooseSDKModel(m.analyticsTracker, m.accessToken, m.baseUri, m.sdk.index) cmd = m.currentModel.Init() case stepToggleFlag: m.currentStep -= 1 m.currentModel = NewShowSDKInstructionsModel( + m.analyticsTracker, m.environmentsClient, m.accessToken, m.baseUri, @@ -110,6 +120,7 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case choseSDKMsg: m.currentModel = NewShowSDKInstructionsModel( + m.analyticsTracker, m.environmentsClient, m.accessToken, m.baseUri, @@ -123,7 +134,8 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.sdk = msg.sdk m.currentStep += 1 case confirmedFlagMsg: - m.currentModel = NewChooseSDKModel(0) + m.currentModel = NewChooseSDKModel(m.analyticsTracker, m.accessToken, m.baseUri, 0) + cmd = m.currentModel.Init() m.flagKey = msg.flag.key m.currentStep += 1 m.err = nil @@ -142,11 +154,13 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.err = nil case showToggleFlagMsg: m.currentModel = NewToggleFlagModel( + m.analyticsTracker, m.flagsClient, m.accessToken, m.baseUri, m.flagKey, m.sdk.kind, + m.startTime, ) cmd = m.currentModel.Init() m.currentStep += 1 diff --git a/internal/quickstart/create_flag.go b/internal/quickstart/create_flag.go index d7bc76aa..a419536d 100644 --- a/internal/quickstart/create_flag.go +++ b/internal/quickstart/create_flag.go @@ -8,7 +8,10 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/spf13/viper" + "ldcli/cmd/cliflags" + "ldcli/internal/analytics" "ldcli/internal/flags" ) @@ -21,6 +24,7 @@ type flag struct { type createFlagModel struct { accessToken string + analyticsTracker analytics.Tracker baseUri string client flags.Client err error @@ -32,7 +36,7 @@ type createFlagModel struct { textInput textinput.Model } -func NewCreateFlagModel(client flags.Client, accessToken, baseUri string) tea.Model { +func NewCreateFlagModel(analyticsTracker analytics.Tracker, client flags.Client, accessToken, baseUri string) tea.Model { ti := textinput.New() ti.Focus() ti.CharLimit = 156 @@ -40,10 +44,11 @@ func NewCreateFlagModel(client flags.Client, accessToken, baseUri string) tea.Mo ti.Prompt = "" return createFlagModel{ - accessToken: accessToken, - baseUri: baseUri, - client: client, - help: help.New(), + analyticsTracker: analyticsTracker, + accessToken: accessToken, + baseUri: baseUri, + client: client, + help: help.New(), helpKeys: keyMap{ Quit: BindingQuit, }, @@ -52,6 +57,8 @@ func NewCreateFlagModel(client flags.Client, accessToken, baseUri string) tea.Mo } func (m createFlagModel) Init() tea.Cmd { + m.analyticsTracker.SendSetupStartedEvent(m.accessToken, m.baseUri, viper.GetBool(cliflags.AnalyticsOptOut), "1 - feature flag name") + return nil } diff --git a/internal/quickstart/show_sdk_instructions.go b/internal/quickstart/show_sdk_instructions.go index 1c791129..70eb7f28 100644 --- a/internal/quickstart/show_sdk_instructions.go +++ b/internal/quickstart/show_sdk_instructions.go @@ -10,7 +10,10 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" + "github.com/spf13/viper" + "ldcli/cmd/cliflags" + "ldcli/internal/analytics" "ldcli/internal/environments" "ldcli/internal/sdks" ) @@ -28,6 +31,7 @@ type environment struct { type showSDKInstructionsModel struct { accessToken string + analyticsTracker analytics.Tracker baseUri string canonicalName string displayName string @@ -44,6 +48,7 @@ type showSDKInstructionsModel struct { } func NewShowSDKInstructionsModel( + analyticsTracker analytics.Tracker, environmentsClient environments.Client, accessToken string, baseUri string, @@ -67,6 +72,7 @@ func NewShowSDKInstructionsModel( h := help.New() return showSDKInstructionsModel{ + analyticsTracker: analyticsTracker, accessToken: accessToken, baseUri: baseUri, canonicalName: canonicalName, @@ -92,6 +98,9 @@ func NewShowSDKInstructionsModel( // fetch SDK instructions // fetch the environment to get values to interpolate into the instructions func (m showSDKInstructionsModel) Init() tea.Cmd { + m.analyticsTracker.SendSetupStartedEvent(m.accessToken, m.baseUri, viper.GetBool(cliflags.AnalyticsOptOut), "3 - sdk installation") + m.analyticsTracker.SendSetupSDKSelectedEvent(m.accessToken, m.baseUri, viper.GetBool(cliflags.AnalyticsOptOut), m.canonicalName) + cmds := []tea.Cmd{m.spinner.Tick, readSDKInstructions(m.canonicalName)} if m.environment == nil { diff --git a/internal/quickstart/toggle_flag.go b/internal/quickstart/toggle_flag.go index 9041b0f6..e753a041 100644 --- a/internal/quickstart/toggle_flag.go +++ b/internal/quickstart/toggle_flag.go @@ -2,52 +2,64 @@ package quickstart import ( "fmt" + "time" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/spf13/viper" + "ldcli/cmd/cliflags" + "ldcli/internal/analytics" "ldcli/internal/errors" "ldcli/internal/flags" ) type toggleFlagModel struct { - accessToken string - baseUri string - client flags.Client - enabled bool - err error - flagKey string - flagWasEnabled bool - flagWasFetched bool - help help.Model - helpKeys keyMap - sdkKind string - spinner spinner.Model + accessToken string + analyticsTracker analytics.Tracker + baseUri string + endTime time.Time + client flags.Client + enabled bool + err error + flagKey string + flagWasEnabled bool + flagWasFetched bool + help help.Model + helpKeys keyMap + sdkKind string + spinner spinner.Model + setupStartTime time.Time + toggleCount int } -func NewToggleFlagModel(client flags.Client, accessToken string, baseUri string, flagKey string, sdkKind string) tea.Model { +func NewToggleFlagModel(analyticsTracker analytics.Tracker, client flags.Client, accessToken string, baseUri string, flagKey string, sdkKind string, startTime time.Time) tea.Model { s := spinner.New() s.Spinner = spinner.Points return toggleFlagModel{ - accessToken: accessToken, - baseUri: baseUri, - client: client, - flagKey: flagKey, - help: help.New(), + analyticsTracker: analyticsTracker, + accessToken: accessToken, + baseUri: baseUri, + client: client, + flagKey: flagKey, + help: help.New(), helpKeys: keyMap{ Back: BindingBack, Quit: BindingQuit, }, - sdkKind: sdkKind, - spinner: s, + sdkKind: sdkKind, + spinner: s, + setupStartTime: startTime, } } func (m toggleFlagModel) Init() tea.Cmd { + m.analyticsTracker.SendSetupStartedEvent(m.accessToken, m.baseUri, viper.GetBool(cliflags.AnalyticsOptOut), "4 - flag toggle") + cmds := []tea.Cmd{ m.spinner.Tick, fetchFlagStatus(m.client, m.accessToken, m.baseUri, m.flagKey, defaultEnvKey, defaultProjKey), @@ -73,8 +85,20 @@ func (m toggleFlagModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, toggleFlag(m.client, m.accessToken, m.baseUri, m.flagKey, m.enabled) } case fetchedFlagStatusMsg: - m.enabled = msg.enabled + if !m.flagWasFetched { + m.endTime = time.Now() + } m.flagWasFetched = true + m.enabled = msg.enabled + m.toggleCount++ + m.analyticsTracker.SendSetupFlagToggledEvent( + m.accessToken, + m.baseUri, + viper.GetBool(cliflags.AnalyticsOptOut), + m.enabled, + m.toggleCount, + m.endTime.Sub(m.setupStartTime).Milliseconds(), + ) case spinner.TickMsg: m.spinner, cmd = m.spinner.Update(msg) case errMsg: