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 ShutdownTimeout: 500 * time.Millisecond,
45 })
46 if err != nil {
47 slog.Error("Failed to initialize PostHog client", "error", err)
48 }
49 client = c
50 distinctId = getDistinctId()
51}
52
53func GetID() string { return distinctId }
54
55func Alias(userID string) {
56 if client == nil || distinctId == fallbackId || distinctId == "" || userID == "" {
57 return
58 }
59 if err := client.Enqueue(posthog.Alias{
60 DistinctId: distinctId,
61 Alias: userID,
62 }); err != nil {
63 slog.Error("Failed to enqueue PostHog alias event", "error", err)
64 return
65 }
66 slog.Info("Aliased in PostHog", "machine_id", distinctId, "user_id", userID)
67}
68
69// send logs an event to PostHog with the given event name and properties.
70func send(event string, props ...any) {
71 if client == nil {
72 return
73 }
74 err := client.Enqueue(posthog.Capture{
75 DistinctId: distinctId,
76 Event: event,
77 Properties: pairsToProps(props...).Merge(baseProps),
78 })
79 if err != nil {
80 slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err)
81 return
82 }
83}
84
85// Error logs an error event to PostHog with the error type and message.
86func Error(errToLog any, props ...any) {
87 if client == nil {
88 return
89 }
90 posthogErr := client.Enqueue(posthog.NewDefaultException(
91 time.Now(),
92 distinctId,
93 reflect.TypeOf(errToLog).String(),
94 fmt.Sprintf("%v", errToLog),
95 ))
96 if posthogErr != nil {
97 slog.Error("Failed to enqueue PostHog error", "err", errToLog, "props", props, "posthogErr", posthogErr)
98 return
99 }
100}
101
102func Flush() {
103 if client == nil {
104 return
105 }
106 if err := client.Close(); err != nil {
107 slog.Error("Failed to flush PostHog events", "error", err)
108 }
109}
110
111func pairsToProps(props ...any) posthog.Properties {
112 p := posthog.NewProperties()
113
114 if !isEven(len(props)) {
115 slog.Error("Event properties must be provided as key-value pairs", "props", props)
116 return p
117 }
118
119 for i := 0; i < len(props); i += 2 {
120 key := props[i].(string)
121 value := props[i+1]
122 p = p.Set(key, value)
123 }
124 return p
125}
126
127func isEven(n int) bool {
128 return n%2 == 0
129}