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 posthogErr := client.Enqueue(posthog.NewDefaultException(
101 time.Now(),
102 distinctId,
103 reflect.TypeOf(errToLog).String(),
104 fmt.Sprintf("%v", errToLog),
105 ))
106 if posthogErr != nil {
107 slog.Error("Failed to enqueue PostHog error", "err", errToLog, "props", props, "posthogErr", posthogErr)
108 return
109 }
110}
111
112func Flush() {
113 if client == nil {
114 return
115 }
116 if err := client.Close(); err != nil {
117 slog.Error("Failed to flush PostHog events", "error", err)
118 }
119}
120
121func pairsToProps(props ...any) posthog.Properties {
122 p := posthog.NewProperties()
123
124 if !isEven(len(props)) {
125 slog.Error("Event properties must be provided as key-value pairs", "props", props)
126 return p
127 }
128
129 for i := 0; i < len(props); i += 2 {
130 key := props[i].(string)
131 value := props[i+1]
132 p = p.Set(key, value)
133 }
134 return p
135}
136
137func isEven(n int) bool {
138 return n%2 == 0
139}