Skip to content

Commit f961a92

Browse files
authored
feat(analytics): add opt in PostHog analytics (#293)
* feat(analytics): add PostHog analytics for CLI command tracking Add comprehensive, privacy-conscious analytics using PostHog that captures every command invocation with args, flags, execution context, and system info. Users are prompted to opt in during login, with preference stored in personal_settings.json. Tracked properties include: command path, duration, success/failure, OS/arch, CLI version, TTY/pipe detection, CI/SSH detection, shell, locale, timezone, GPU info, parent process, and cwd. * chore(analytics): update PostHog project token * chore: update analytics opt-in prompt wording * chore: update analytics opt-in choice wording * feat(analytics): add is_stdout_piped property * chore(analytics): remove parent_cmdline property * chore(analytics): restore parent_cmdline property * fix: wrap errors to satisfy wrapcheck linter
1 parent a01d0b5 commit f961a92

10 files changed

Lines changed: 455 additions & 9 deletions

File tree

go.mod

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ require (
2424
github.com/manifoldco/promptui v0.9.0
2525
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
2626
github.com/pkg/errors v0.9.1
27+
github.com/posthog/posthog-go v1.10.0
2728
github.com/robfig/cron/v3 v3.0.1
2829
github.com/samber/lo v1.33.0
2930
github.com/samber/mo v1.5.1
@@ -33,7 +34,7 @@ require (
3334
github.com/spf13/afero v1.9.2
3435
github.com/spf13/cobra v1.8.1
3536
github.com/spf13/viper v1.13.0
36-
github.com/stretchr/testify v1.10.0
37+
github.com/stretchr/testify v1.11.1
3738
github.com/tidwall/gjson v1.14.0
3839
github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7
3940
github.com/wk8/go-ordered-map/v2 v2.0.0
@@ -67,9 +68,10 @@ require (
6768
github.com/go-playground/locales v0.14.1 // indirect
6869
github.com/go-playground/universal-translator v0.18.1 // indirect
6970
github.com/go-playground/validator/v10 v10.20.0 // indirect
70-
github.com/goccy/go-json v0.10.2 // indirect
71+
github.com/goccy/go-json v0.10.5 // indirect
7172
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
7273
github.com/google/gnostic-models v0.6.8 // indirect
74+
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
7375
github.com/hashicorp/hcl v1.0.0 // indirect
7476
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
7577
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
@@ -138,7 +140,7 @@ require (
138140
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
139141
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
140142
github.com/rivo/uniseg v0.2.0 // indirect
141-
github.com/spf13/pflag v1.0.5 // indirect
143+
github.com/spf13/pflag v1.0.5
142144
github.com/xlab/treeprint v1.2.0 // indirect
143145
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
144146
golang.org/x/net v0.48.0 // indirect

go.sum

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+
157157
github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
158158
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
159159
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
160-
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
161-
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
160+
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
161+
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
162162
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
163163
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
164164
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
@@ -254,6 +254,8 @@ github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca
254254
github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
255255
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
256256
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
257+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
258+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
257259
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
258260
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
259261
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -356,6 +358,8 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
356358
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
357359
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
358360
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
361+
github.com/posthog/posthog-go v1.10.0 h1:wfoy7Jfb4LigCoHYyMZoiJmmEoCLOkSaYfDxM/NtCqY=
362+
github.com/posthog/posthog-go v1.10.0/go.mod h1:wB3/9Q7d9gGb1P/yf/Wri9VBlbP8oA8z++prRzL5OcY=
359363
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
360364
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
361365
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -408,8 +412,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
408412
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
409413
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
410414
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
411-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
412-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
415+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
416+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
413417
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
414418
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
415419
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=

main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"os"
55

6+
"github.com/brevdev/brev-cli/pkg/analytics"
67
"github.com/brevdev/brev-cli/pkg/cmd"
78
"github.com/brevdev/brev-cli/pkg/cmd/cmderrors"
89
"github.com/brevdev/brev-cli/pkg/errors"
@@ -11,9 +12,11 @@ import (
1112
func main() {
1213
done := errors.GetDefaultErrorReporter().Setup()
1314
defer done()
15+
defer analytics.Close()
1416
command := cmd.NewDefaultBrevCommand()
1517

1618
if err := command.Execute(); err != nil {
19+
analytics.CaptureCommandError()
1720
cmderrors.DisplayAndHandleError(err)
1821
done()
1922
os.Exit(1) //nolint:gocritic // manually call done

pkg/analytics/parentprocess.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package analytics
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"runtime"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
// getParentProcessInfo returns the name and full command line of the parent process.
12+
func getParentProcessInfo() (name, cmdline string) {
13+
ppid := os.Getppid()
14+
if ppid <= 0 {
15+
return "", ""
16+
}
17+
18+
switch runtime.GOOS {
19+
case "linux":
20+
return getParentProcessLinux(ppid)
21+
case "darwin":
22+
return getParentProcessDarwin(ppid)
23+
default:
24+
return "", ""
25+
}
26+
}
27+
28+
func getParentProcessLinux(ppid int) (name, cmdline string) {
29+
pidStr := strconv.Itoa(ppid)
30+
31+
commBytes, err := os.ReadFile("/proc/" + pidStr + "/comm")
32+
if err == nil {
33+
name = strings.TrimSpace(string(commBytes))
34+
}
35+
36+
cmdlineBytes, err := os.ReadFile("/proc/" + pidStr + "/cmdline")
37+
if err == nil {
38+
// /proc cmdline uses null bytes as separators
39+
cmdline = strings.ReplaceAll(string(cmdlineBytes), "\x00", " ")
40+
cmdline = strings.TrimSpace(cmdline)
41+
}
42+
43+
return name, cmdline
44+
}
45+
46+
func getParentProcessDarwin(ppid int) (name, cmdline string) {
47+
pidStr := strconv.Itoa(ppid)
48+
49+
out, err := exec.Command("ps", "-p", pidStr, "-o", "comm=").Output() // #nosec G204
50+
if err == nil {
51+
name = strings.TrimSpace(string(out))
52+
}
53+
54+
out, err = exec.Command("ps", "-p", pidStr, "-o", "args=").Output() // #nosec G204
55+
if err == nil {
56+
cmdline = strings.TrimSpace(string(out))
57+
}
58+
59+
return name, cmdline
60+
}

0 commit comments

Comments
 (0)