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