1package event
2
3import (
4 "fmt"
5 "log/slog"
6 "os"
7 "path/filepath"
8 "reflect"
9 "runtime"
10 "time"
11
12 "github.com/charmbracelet/crush/internal/version"
13 "github.com/posthog/posthog-go"
14)
15
16const (
17 endpoint = "https://data.charm.land"
18 key = "phc_4zt4VgDWLqbYnJYEwLRxFoaTL2noNrQij0C6E8k3I0V"
19
20 nonInteractiveEventName = "NonInteractive"
21)
22
23var (
24 client posthog.Client
25
26 baseProps = posthog.NewProperties().
27 Set("GOOS", runtime.GOOS).
28 Set("GOARCH", runtime.GOARCH).
29 Set("TERM", os.Getenv("TERM")).
30 Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
31 Set("Version", version.Version).
32 Set("GoVersion", runtime.Version()).
33 Set(nonInteractiveEventName, false)
34)
35
36func SetNonInteractive(nonInteractive bool) {
37 baseProps = baseProps.Set(nonInteractiveEventName, nonInteractive)
38}
39
40func Init() {
41 c, err := posthog.NewWithConfig(key, posthog.Config{
42 Endpoint: endpoint,
43 Logger: logger{},
44 })
45 if err != nil {
46 slog.Error("Failed to initialize PostHog client", "error", err)
47 }
48 client = c
49 distinctId = getDistinctId()
50}
51
52func GetID() string { return distinctId }
53
54func Alias(userID string) {
55 if client == nil || distinctId == fallbackId || distinctId == "" || userID == "" {
56 return
57 }
58 if err := client.Enqueue(posthog.Alias{
59 DistinctId: distinctId,
60 Alias: userID,
61 }); err != nil {
62 slog.Error("Failed to enqueue PostHog alias event", "error", err)
63 return
64 }
65 slog.Info("Aliased in PostHog", "machine_id", distinctId, "user_id", userID)
66}
67
68// send logs an event to PostHog with the given event name and properties.
69func send(event string, props ...any) {
70 if client == nil {
71 return
72 }
73 err := client.Enqueue(posthog.Capture{
74 DistinctId: distinctId,
75 Event: event,
76 Properties: pairsToProps(props...).Merge(baseProps),
77 })
78 if err != nil {
79 slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err)
80 return
81 }
82}
83
84// Error logs an error event to PostHog with the error type and message.
85func Error(err any, props ...any) {
86 if client == nil {
87 return
88 }
89 posthogErr := client.Enqueue(posthog.NewDefaultException(
90 time.Now(),
91 distinctId,
92 reflect.TypeOf(err).String(),
93 fmt.Sprintf("%v", err),
94 ))
95 if err != nil {
96 slog.Error("Failed to enqueue PostHog error", "err", err, "props", props, "posthogErr", posthogErr)
97 return
98 }
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}