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	nonInteractiveEventName = "NonInteractive"
 20)
 21
 22var (
 23	client posthog.Client
 24
 25	baseProps = posthog.NewProperties().
 26			Set("GOOS", runtime.GOOS).
 27			Set("GOARCH", runtime.GOARCH).
 28			Set("TERM", os.Getenv("TERM")).
 29			Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
 30			Set("Version", version.Version).
 31			Set("GoVersion", runtime.Version()).
 32			Set(nonInteractiveEventName, false)
 33)
 34
 35func SetNonInteractive(nonInteractive bool) {
 36	baseProps = baseProps.Set(nonInteractiveEventName, nonInteractive)
 37}
 38
 39func Init() {
 40	c, err := posthog.NewWithConfig(key, posthog.Config{
 41		Endpoint: endpoint,
 42		Logger:   logger{},
 43	})
 44	if err != nil {
 45		slog.Error("Failed to initialize PostHog client", "error", err)
 46	}
 47	client = c
 48	distinctId = getDistinctId()
 49}
 50
 51func GetID() string { return distinctId }
 52
 53func Alias(userID string) {
 54	if client == nil || distinctId == fallbackId || distinctId == "" || userID == "" {
 55		return
 56	}
 57	if err := client.Enqueue(posthog.Alias{
 58		DistinctId: distinctId,
 59		Alias:      userID,
 60	}); err != nil {
 61		slog.Error("Failed to enqueue PostHog alias event", "error", err)
 62		return
 63	}
 64	slog.Info("Aliased in PostHog", "machine_id", distinctId, "user_id", userID)
 65}
 66
 67// send logs an event to PostHog with the given event name and properties.
 68func send(event string, props ...any) {
 69	if client == nil {
 70		return
 71	}
 72	err := client.Enqueue(posthog.Capture{
 73		DistinctId: distinctId,
 74		Event:      event,
 75		Properties: pairsToProps(props...).Merge(baseProps),
 76	})
 77	if err != nil {
 78		slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err)
 79		return
 80	}
 81}
 82
 83// Error logs an error event to PostHog with the error type and message.
 84func Error(err any, props ...any) {
 85	if client == nil {
 86		return
 87	}
 88	// The PostHog Go client does not yet support sending exceptions.
 89	// We're mimicking the behavior by sending the minimal info required
 90	// for PostHog to recognize this as an exception event.
 91	props = append(
 92		[]any{
 93			"$exception_list",
 94			[]map[string]string{
 95				{"type": reflect.TypeOf(err).String(), "value": fmt.Sprintf("%v", err)},
 96			},
 97		},
 98		props...,
 99	)
100	send("$exception", props...)
101}
102
103func Flush() {
104	if client == nil {
105		return
106	}
107	if err := client.Close(); err != nil {
108		slog.Error("Failed to flush PostHog events", "error", err)
109	}
110}
111
112func pairsToProps(props ...any) posthog.Properties {
113	p := posthog.NewProperties()
114
115	if !isEven(len(props)) {
116		slog.Error("Event properties must be provided as key-value pairs", "props", props)
117		return p
118	}
119
120	for i := 0; i < len(props); i += 2 {
121		key := props[i].(string)
122		value := props[i+1]
123		p = p.Set(key, value)
124	}
125	return p
126}
127
128func isEven(n int) bool {
129	return n%2 == 0
130}