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	})
 45	if err != nil {
 46		slog.Error("Failed to initialize PostHog client", "error", err)
 47	}
 48	client = c
 49	distinctId = getDistinctId()
 50}
 51
 52func GetID() string { return distinctId }
 53
 54func Alias(userID string) {
 55	if client == nil || distinctId == fallbackId || distinctId == "" || userID == "" {
 56		return
 57	}
 58	if err := client.Enqueue(posthog.Alias{
 59		DistinctId: distinctId,
 60		Alias:      userID,
 61	}); err != nil {
 62		slog.Error("Failed to enqueue PostHog alias event", "error", err)
 63		return
 64	}
 65	slog.Info("Aliased in PostHog", "machine_id", distinctId, "user_id", userID)
 66}
 67
 68// send logs an event to PostHog with the given event name and properties.
 69func send(event string, props ...any) {
 70	if client == nil {
 71		return
 72	}
 73	err := client.Enqueue(posthog.Capture{
 74		DistinctId: distinctId,
 75		Event:      event,
 76		Properties: pairsToProps(props...).Merge(baseProps),
 77	})
 78	if err != nil {
 79		slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err)
 80		return
 81	}
 82}
 83
 84// Error logs an error event to PostHog with the error type and message.
 85func Error(err any, props ...any) {
 86	if client == nil {
 87		return
 88	}
 89	posthogErr := client.Enqueue(posthog.NewDefaultException(
 90		time.Now(),
 91		distinctId,
 92		reflect.TypeOf(err).String(),
 93		fmt.Sprintf("%v", err),
 94	))
 95	if err != nil {
 96		slog.Error("Failed to enqueue PostHog error", "err", err, "props", props, "posthogErr", posthogErr)
 97		return
 98	}
 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}