mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-18 11:06:19 +00:00
* feat: add PostHog usage analytics to CLI Add anonymous command-level analytics to browseros-cli using the PostHog Go SDK. Tracks which commands are executed, their success/failure status, and duration — no PII or person profiles. - New analytics package with Init/Track/Close singleton - Distinct ID resolves from server's browseros_id (server.json), falls back to CLI-generated UUID (~/.config/browseros-cli/install_id) - API key injected at build time via ldflags (dev builds = silent no-op) - Server now writes browseros_id into server.json for cross-surface identity correlation * fix: address PR review feedback for #603 - Return "unknown" for unrecognized args in commandName to avoid sending arbitrary user input to PostHog - Revert goreleaser to {{ .Env.POSTHOG_API_KEY }} (intentional hard fail — release builds must have the key set) - go mod tidy to fix posthog-go direct/indirect marker - Add POSTHOG_API_KEY to .env.production.example
130 lines
2.4 KiB
Go
130 lines
2.4 KiB
Go
package analytics
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"browseros-cli/config"
|
|
|
|
"github.com/posthog/posthog-go"
|
|
)
|
|
|
|
var (
|
|
posthogAPIKey string // set via ldflags
|
|
posthogHost = "https://us.i.posthog.com"
|
|
)
|
|
|
|
const eventPrefix = "browseros.cli."
|
|
|
|
var svc *service
|
|
|
|
type service struct {
|
|
client posthog.Client
|
|
distinctID string
|
|
}
|
|
|
|
func Init(version string) {
|
|
if posthogAPIKey == "" {
|
|
return
|
|
}
|
|
|
|
distinctID := resolveDistinctID()
|
|
if distinctID == "" {
|
|
return
|
|
}
|
|
|
|
client, err := posthog.NewWithConfig(posthogAPIKey, posthog.Config{
|
|
Endpoint: posthogHost,
|
|
BatchSize: 10,
|
|
ShutdownTimeout: 3 * time.Second,
|
|
DefaultEventProperties: posthog.NewProperties().
|
|
Set("cli_version", version).
|
|
Set("os", runtime.GOOS).
|
|
Set("arch", runtime.GOARCH),
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
svc = &service{client: client, distinctID: distinctID}
|
|
}
|
|
|
|
func Track(command string, success bool, duration time.Duration) {
|
|
if svc == nil {
|
|
return
|
|
}
|
|
svc.client.Enqueue(posthog.Capture{
|
|
DistinctId: svc.distinctID,
|
|
Event: eventPrefix + "command_executed",
|
|
Properties: posthog.NewProperties().
|
|
Set("command", command).
|
|
Set("success", success).
|
|
Set("duration_ms", duration.Milliseconds()).
|
|
Set("$process_person_profile", false),
|
|
})
|
|
}
|
|
|
|
func Close() {
|
|
if svc == nil {
|
|
return
|
|
}
|
|
svc.client.Close()
|
|
svc = nil
|
|
}
|
|
|
|
func resolveDistinctID() string {
|
|
if id := loadBrowserosID(); id != "" {
|
|
return id
|
|
}
|
|
return loadOrCreateInstallID()
|
|
}
|
|
|
|
func loadBrowserosID() string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(home, ".browseros", "server.json"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var sc struct {
|
|
BrowserosID string `json:"browseros_id"`
|
|
}
|
|
if json.Unmarshal(data, &sc) != nil {
|
|
return ""
|
|
}
|
|
return sc.BrowserosID
|
|
}
|
|
|
|
func loadOrCreateInstallID() string {
|
|
dir := config.Dir()
|
|
idPath := filepath.Join(dir, "install_id")
|
|
|
|
data, err := os.ReadFile(idPath)
|
|
if err == nil {
|
|
if id := strings.TrimSpace(string(data)); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
|
|
id := generateUUID()
|
|
os.MkdirAll(dir, 0755)
|
|
os.WriteFile(idPath, []byte(id), 0644)
|
|
return id
|
|
}
|
|
|
|
func generateUUID() string {
|
|
var b [16]byte
|
|
rand.Read(b[:])
|
|
b[6] = (b[6] & 0x0f) | 0x40 // version 4
|
|
b[8] = (b[8] & 0x3f) | 0x80 // variant 2
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
|
}
|