1package event
2
3import (
4 "fmt"
5 "log/slog"
6 "os"
7 "path/filepath"
8 "reflect"
9 "runtime"
10
11 "github.com/charmbracelet/crush/internal/version"
12 "github.com/denisbrodbeck/machineid"
13 "github.com/posthog/posthog-go"
14)
15
16const (
17 endpoint = "https://data.charm.land"
18 key = "phc_4zt4VgDWLqbYnJYEwLRxFoaTL2noNrQij0C6E8k3I0V"
19)
20
21var (
22 client posthog.Client
23
24 baseProps = posthog.NewProperties().
25 Set("GOOS", runtime.GOOS).
26 Set("GOARCH", runtime.GOARCH).
27 Set("TERM", os.Getenv("TERM")).
28 Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
29 Set("Version", version.Version).
30 Set("GoVersion", runtime.Version())
31)
32
33func Init() {
34 c, err := posthog.NewWithConfig(key, posthog.Config{
35 Endpoint: endpoint,
36 Logger: logger{},
37 })
38 if err != nil {
39 slog.Error("Failed to initialize PostHog client", "error", err)
40 }
41 client = c
42}
43
44// send logs an event to PostHog with the given event name and properties.
45func send(event string, props ...any) {
46 if client == nil {
47 return
48 }
49 err := client.Enqueue(posthog.Capture{
50 DistinctId: distinctId(),
51 Event: event,
52 Properties: pairsToProps(props...).Merge(baseProps),
53 })
54 if err != nil {
55 slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err)
56 return
57 }
58}
59
60// Error logs an error event to PostHog with the error type and message.
61func Error(err any, props ...any) {
62 if client == nil {
63 return
64 }
65 // The PostHog Go client does not yet support sending exceptions.
66 // We're mimicking the behavior by sending the minimal info required
67 // for PostHog to recognize this as an exception event.
68 props = append(
69 []any{
70 "$exception_list",
71 []map[string]string{
72 {"type": reflect.TypeOf(err).String(), "value": fmt.Sprintf("%v", err)},
73 },
74 },
75 props...,
76 )
77 send("$exception", props...)
78}
79
80func Flush() {
81 if client == nil {
82 return
83 }
84 if err := client.Close(); err != nil {
85 slog.Error("Failed to flush PostHog events", "error", err)
86 }
87}
88
89func pairsToProps(props ...any) posthog.Properties {
90 p := posthog.NewProperties()
91
92 if !isEven(len(props)) {
93 slog.Error("Event properties must be provided as key-value pairs", "props", props)
94 return p
95 }
96
97 for i := 0; i < len(props); i += 2 {
98 key := props[i].(string)
99 value := props[i+1]
100 p = p.Set(key, value)
101 }
102 return p
103}
104
105func isEven(n int) bool {
106 return n%2 == 0
107}
108
109func distinctId() string {
110 id, err := machineid.ProtectedID("charm")
111 if err != nil {
112 return "crush-cli"
113 }
114 return id
115}