event.go

  1package event
  2
  3import (
  4	"fmt"
  5	"log/slog"
  6	"os"
  7	"reflect"
  8	"runtime"
  9
 10	"github.com/charmbracelet/crush/internal/version"
 11	"github.com/denisbrodbeck/machineid"
 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", os.Getenv("SHELL")).
 28			Set("Version", version.Version).
 29			Set("GoVersion", runtime.Version())
 30)
 31
 32func Init() {
 33	c, err := posthog.NewWithConfig(key, posthog.Config{
 34		Endpoint: endpoint,
 35		Logger:   logger{},
 36	})
 37	if err != nil {
 38		slog.Error("Failed to initialize PostHog client", "error", err)
 39	}
 40	client = c
 41}
 42
 43// send logs an event to PostHog with the given event name and properties.
 44func send(event string, props ...any) {
 45	if client == nil {
 46		return
 47	}
 48	err := client.Enqueue(posthog.Capture{
 49		DistinctId: distinctId(),
 50		Event:      event,
 51		Properties: pairsToProps(props...).Merge(baseProps),
 52	})
 53	if err != nil {
 54		slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err)
 55		return
 56	}
 57}
 58
 59// Error logs an error event to PostHog with the error type and message.
 60func Error(err any, props ...any) {
 61	if client == nil {
 62		return
 63	}
 64	// The PostHog Go client does not yet support sending exceptions.
 65	// We're mimicking the behavior by sending the minimal info required
 66	// for PostHog to recognize this as an exception event.
 67	props = append(
 68		[]any{
 69			"$exception_list",
 70			[]map[string]string{
 71				{"type": reflect.TypeOf(err).String(), "value": fmt.Sprintf("%v", err)},
 72			},
 73		},
 74		props...,
 75	)
 76	send("$exception", props...)
 77}
 78
 79func Flush() {
 80	if client == nil {
 81		return
 82	}
 83	if err := client.Close(); err != nil {
 84		slog.Error("Failed to flush PostHog events", "error", err)
 85	}
 86}
 87
 88func pairsToProps(props ...any) posthog.Properties {
 89	p := posthog.NewProperties()
 90
 91	if !isEven(len(props)) {
 92		slog.Error("Event properties must be provided as key-value pairs", "props", props)
 93		return p
 94	}
 95
 96	for i := 0; i < len(props); i += 2 {
 97		key := props[i].(string)
 98		value := props[i+1]
 99		p = p.Set(key, value)
100	}
101	return p
102}
103
104func isEven(n int) bool {
105	return n%2 == 0
106}
107
108func distinctId() string {
109	id, err := machineid.ProtectedID("charm")
110	if err != nil {
111		return "crush-cli"
112	}
113	return id
114}