// 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)
}
}