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