Files
zeekay 702f083718 rebrand: zero posthog, no compat
Switch from github.com/posthog/posthog-go to
github.com/hanzoai/insights-go across all imports,
type references, and go.mod/go.sum.
2026-03-02 00:27:52 -07:00

162 lines
4.9 KiB
Go

// Copyright (C) 2020-2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package utils
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/luxfi/cli/pkg/types"
"github.com/luxfi/cli/pkg/ux"
insights "github.com/hanzoai/insights-go"
"github.com/spf13/cobra"
)
// telemetryToken value is set at build and install scripts using ldflags
var (
telemetryToken = ""
telemetryInstance = "https://insights.hanzo.ai"
)
func GetCLIVersion() string {
wdPath, err := os.Getwd()
if err != nil {
return ""
}
versionPath := filepath.Join(wdPath, "VERSION")
content, err := os.ReadFile(versionPath) //nolint:gosec // G304: Reading VERSION file from current directory
if err != nil {
return ""
}
return string(content)
}
func PrintMetricsOptOutPrompt() {
ux.Logger.PrintToUser(
"Lux CLI (the \"software\") may collect statistical data on how the software is used on an anonymous " +
"basis for purposes of product improvement. This data will not (i) include any passwords, scripts, or data " +
"files, (ii) be associated with any particular user or entity, or (iii) include any personally identifiable " +
"information or be used to identify individuals or entities using the software. You can disable such data " +
"collection with `lux config metrics disable` command, which will result in no data being collected; " +
"by using the software without so disabling such data collection you expressly consent to the collection of " +
"such data. You can also read our privacy statement <https://lux.network/privacy> to learn more. \n")
ux.Logger.PrintToUser("You can disable data collection with `lux config metrics disable` command. " +
"You can also read our privacy statement <https://lux.network/privacy> to learn more.\n")
}
func saveMetricsConfig(writer types.ConfigWriter, metricsEnabled bool) {
cfg := types.Config{MetricsEnabled: metricsEnabled}
jsonBytes, _ := json.Marshal(&cfg)
_ = writer.WriteConfigFile(jsonBytes)
}
// isInteractiveTerminal returns true if stdin is a terminal (not piped/redirected)
func isInteractiveTerminal() bool {
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
// Check if stdin is a character device (terminal)
return (fi.Mode() & os.ModeCharDevice) != 0
}
func HandleUserMetricsPreference(app interface{}) error {
writer, ok := app.(types.ConfigWriter)
if !ok {
return fmt.Errorf("app does not implement ConfigWriter")
}
// In non-interactive mode (CI, scripts, piped input), default to opt-out
// and skip the prompt entirely to avoid blocking
if !isInteractiveTerminal() {
ux.Logger.PrintToUser("Non-interactive mode detected - metrics collection disabled by default")
ux.Logger.PrintToUser("Run 'lux config metrics enable' to opt-in")
saveMetricsConfig(writer, false)
return nil
}
prompter, ok := app.(types.PrompterInterface)
if !ok {
return fmt.Errorf("app does not implement PrompterInterface")
}
PrintMetricsOptOutPrompt()
txt := "Press [Enter] to opt-in, or opt out by choosing 'No'"
yes, err := prompter.CaptureYesNo(txt)
if err != nil {
return err
}
if !yes {
ux.Logger.PrintToUser("Lux CLI usage metrics will not be collected")
} else {
ux.Logger.PrintToUser("Thank you for opting in Lux CLI usage metrics collection")
}
saveMetricsConfig(writer, yes)
return nil
}
func userIsOptedIn(app interface{}) bool {
loader, ok := app.(types.ConfigLoader)
if !ok {
return false
}
// if config file is not found or unable to be read, will return false (user is not opted in)
config, err := loader.LoadConfig()
if err != nil {
return false
}
return config.MetricsEnabled
}
func HandleTracking(cmd *cobra.Command, app interface{}, flags map[string]string) {
if userIsOptedIn(app) {
if !cmd.HasSubCommands() && checkCommandIsNotCompletion(cmd) {
TrackMetrics(cmd, flags)
}
}
}
func checkCommandIsNotCompletion(cmd *cobra.Command) bool {
result := strings.Fields(cmd.CommandPath())
if len(result) >= 2 && result[1] == "completion" {
return false
}
return true
}
func TrackMetrics(command *cobra.Command, flags map[string]string) {
if telemetryToken == "" || os.Getenv("RUN_E2E") != "" {
return
}
client, _ := insights.NewWithConfig(telemetryToken, insights.Config{Endpoint: telemetryInstance})
defer func() { _ = client.Close() }()
usr, _ := user.Current() // use empty string if err
hash := sha256.Sum256([]byte(fmt.Sprintf("%s%s", usr.Username, usr.Uid)))
userID := base64.StdEncoding.EncodeToString(hash[:])
telemetryProperties := make(map[string]interface{})
telemetryProperties["command"] = command.CommandPath()
telemetryProperties["version"] = GetCLIVersion()
telemetryProperties["os"] = runtime.GOOS
for propertyKey, propertyValue := range flags {
telemetryProperties[propertyKey] = propertyValue
}
_ = client.Enqueue(insights.Capture{
DistinctId: userID,
Event: "cli-command",
Properties: telemetryProperties,
})
}