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 nonInteractiveAttrName = "NonInteractive"
21 continueSessionByIDAttrName = "ContinueSessionByID"
22 continueLastSessionAttrName = "ContinueLastSession"
23)
24
25var (
26 client posthog.Client
27
28 baseProps = posthog.NewProperties().
29 Set("GOOS", runtime.GOOS).
30 Set("GOARCH", runtime.GOARCH).
31 Set("TERM", os.Getenv("TERM")).
32 Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
33 Set("Version", version.Version).
34 Set("GoVersion", runtime.Version()).
35 Set(nonInteractiveAttrName, false)
36)
37
38func SetNonInteractive(nonInteractive bool) {
39 baseProps = baseProps.Set(nonInteractiveAttrName, nonInteractive)
40}
41
42func SetContinueBySessionID(continueBySessionID bool) {
43 baseProps = baseProps.Set(continueSessionByIDAttrName, continueBySessionID)
44}
45
46func SetContinueLastSession(continueLastSession bool) {
47 baseProps = baseProps.Set(continueLastSessionAttrName, continueLastSession)
48}
49
50func Init() {
51 c, err := posthog.NewWithConfig(key, posthog.Config{
52 Endpoint: endpoint,
53 Logger: logger{},
54 ShutdownTimeout: 500 * time.Millisecond,
55 })
56 if err != nil {
57 slog.Error("Failed to initialize PostHog client", "error", err)
58 }
59 client = c
60 distinctId = getDistinctId()
61}
62
63func GetID() string { return distinctId }
64
65func Alias(userID string) {
66 if client == nil || distinctId == fallbackId || distinctId == "" || userID == "" {
67 return
68 }
69 if err := client.Enqueue(posthog.Alias{
70 DistinctId: distinctId,
71 Alias: userID,
72 }); err != nil {
73 slog.Error("Failed to enqueue PostHog alias event", "error", err)
74 return
75 }
76 slog.Info("Aliased in PostHog", "machine_id", distinctId, "user_id", userID)
77}
78
79// send logs an event to PostHog with the given event name and properties.
80func send(event string, props ...any) {
81 if client == nil {
82 return
83 }
84 err := client.Enqueue(posthog.Capture{
85 DistinctId: distinctId,
86 Event: event,
87 Properties: pairsToProps(props...).Merge(baseProps),
88 })
89 if err != nil {
90 slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err)
91 return
92 }
93}
94
95// Error logs an error event to PostHog with the error type and message.
96func Error(errToLog any, props ...any) {
97 if client == nil || distinctId == "" || errToLog == nil {
98 return
99 }
100
101 exception := posthog.NewDefaultException(
102 time.Now(),
103 distinctId,
104 reflect.TypeOf(errToLog).String(),
105 fmt.Sprintf("%v", errToLog),
106 )
107 if exception.Properties == nil {
108 exception.Properties = posthog.NewProperties()
109 }
110 exception.Properties = exception.Properties.Merge(pairsToProps(props...))
111
112 if posthogErr := client.Enqueue(exception); posthogErr != nil {
113 slog.Error("Failed to enqueue PostHog error", "err", errToLog, "props", props, "posthogErr", posthogErr)
114 return
115 }
116}
117
118func Flush() {
119 if client == nil {
120 return
121 }
122 if err := client.Close(); err != nil {
123 slog.Error("Failed to flush PostHog events", "error", err)
124 }
125}
126
127func pairsToProps(props ...any) posthog.Properties {
128 p := posthog.NewProperties()
129
130 if !isEven(len(props)) {
131 slog.Error("Event properties must be provided as key-value pairs", "props", props)
132 return p
133 }
134
135 for i := 0; i < len(props); i += 2 {
136 key, ok := props[i].(string)
137 if !ok {
138 slog.Error("Event property key must be a string", "key", props[i], "index", i)
139 continue
140 }
141 value := props[i+1]
142 p = p.Set(key, value)
143 }
144 return p
145}
146
147func isEven(n int) bool {
148 return n%2 == 0
149}