event.go

  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}