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