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