event.go

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