// Package analytics sends best-effort product events to posthog. A nil *Client is // valid and drops every event, so callers don't check whether analytics is on. package analytics import ( "log/slog" "time" "github.com/google/uuid" "github.com/posthog/posthog-go" "tangled.org/core/deliberi/config" ) // the same namespace for every event, so an event's uuid only depends on its key var eventNamespace = uuid.MustParse("3f6b2c8e-1d4a-4f7e-9b2a-6c5d8e9f0a1b") type Client struct { ph posthog.Client env string logger *slog.Logger } // New returns nil when no api key is configured func New(cfg config.PosthogConfig, logger *slog.Logger) *Client { if cfg.ApiKey == "" { return nil } ph, err := posthog.NewWithConfig(cfg.ApiKey, posthog.Config{Endpoint: cfg.Endpoint}) if err != nil { logger.Warn("posthog client failed to start, analytics off", "err", err) return nil } logger.Info("posthog analytics enabled", "endpoint", cfg.Endpoint, "env", cfg.Env) return &Client{ph: ph, env: cfg.Env, logger: logger} } // Capture queues an event. key identifies the occurrence (a record version, an // account), so a replayed event keeps its uuid and posthog drops the duplicate func (c *Client) Capture(event, distinctID, key string, properties posthog.Properties) { if c == nil { return } if properties == nil { properties = posthog.NewProperties() } properties.Set("environment", c.env) err := c.ph.Enqueue(posthog.Capture{ Uuid: uuid.NewSHA1(eventNamespace, []byte(event+" "+key)).String(), DistinctId: distinctID, Event: event, Timestamp: time.Now().UTC(), Properties: properties, }) if err != nil { c.logger.Warn("failed to enqueue posthog event", "event", event, "err", err) } } // Close flushes queued events func (c *Client) Close() { if c == nil { return } if err := c.ph.Close(); err != nil { c.logger.Warn("closing posthog client", "err", err) } }