mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
* Add canonical PostHog core metrics events Co-authored-by: multica-agent <github@multica.ai> * Address analytics review feedback Co-authored-by: multica-agent <github@multica.ai> * Tighten analytics review follow-ups Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: multica-agent <github@multica.ai>
129 lines
4.3 KiB
Go
129 lines
4.3 KiB
Go
// Package analytics ships product telemetry events to an external analytics
|
|
// backend (PostHog). Events feed the acquisition → activation → expansion
|
|
// funnel — see docs/analytics.md for the event contract.
|
|
//
|
|
// Design:
|
|
// - Capture is non-blocking. Request handlers must never wait on analytics
|
|
// network I/O, so we enqueue into a bounded channel and a background
|
|
// worker flushes to PostHog in batches.
|
|
// - When the queue is full events are dropped (and counted). A broken
|
|
// analytics backend must never degrade the product.
|
|
// - When POSTHOG_API_KEY is empty the package runs a no-op client, which
|
|
// keeps local dev and self-hosted instances friction-free.
|
|
package analytics
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Event is a single analytics capture. Fields mirror PostHog's /capture/ shape
|
|
// but are framework-agnostic so alternate backends can plug in later.
|
|
type Event struct {
|
|
// Name of the event (e.g. "signup", "workspace_created").
|
|
Name string
|
|
|
|
// DistinctID identifies the person this event belongs to. For logged-in
|
|
// users this is user.id; for anonymous events it should be the anon_id
|
|
// that was previously used on the frontend so identity merging works.
|
|
DistinctID string
|
|
|
|
// WorkspaceID scopes the event to a workspace. Required when the event is
|
|
// about a workspace-level action (workspace_created, issue_executed, ...).
|
|
// Empty is allowed for pre-workspace events (signup).
|
|
WorkspaceID string
|
|
|
|
// Properties is the free-form bag of event attributes. Only serialisable
|
|
// values (string, number, bool, nested maps/slices of the same) should
|
|
// go here. Never put raw PII like full emails here — use email_domain.
|
|
Properties map[string]any
|
|
|
|
// SetOnce properties attach to the person record and are only written the
|
|
// first time they appear. Use this for acquisition attribution
|
|
// (initial_utm_source, etc.) so later events don't overwrite the origin.
|
|
SetOnce map[string]any
|
|
|
|
// Set properties attach to the person record and overwrite on every write.
|
|
// Use this for mutable cohort signals (role, use_case, platform_preference)
|
|
// that users can legitimately change during onboarding.
|
|
Set map[string]any
|
|
|
|
// Timestamp is optional; when zero the client fills in time.Now().
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// Client is the narrow surface the rest of the codebase depends on. Handlers
|
|
// call Capture and move on; the implementation is responsible for buffering,
|
|
// batching, and shipping.
|
|
type Client interface {
|
|
Capture(e Event)
|
|
// Close drains pending events. Call once during graceful shutdown.
|
|
Close()
|
|
}
|
|
|
|
// NewFromEnv returns a Client configured from environment variables:
|
|
//
|
|
// - POSTHOG_API_KEY: project API key. Empty → no-op client.
|
|
// - POSTHOG_HOST: API host (default https://us.i.posthog.com).
|
|
// - ANALYTICS_ENVIRONMENT: production/staging/dev. Defaults from APP_ENV.
|
|
// - ANALYTICS_DISABLED: set to "true"/"1" to force a no-op client even
|
|
// when POSTHOG_API_KEY is set (useful for CI and self-hosted opt-out).
|
|
func NewFromEnv() Client {
|
|
if isDisabled() {
|
|
slog.Info("analytics disabled via ANALYTICS_DISABLED")
|
|
return NoopClient{}
|
|
}
|
|
key := os.Getenv("POSTHOG_API_KEY")
|
|
if key == "" {
|
|
slog.Info("analytics: POSTHOG_API_KEY not set, using noop client")
|
|
return NoopClient{}
|
|
}
|
|
host := os.Getenv("POSTHOG_HOST")
|
|
if host == "" {
|
|
host = "https://us.i.posthog.com"
|
|
}
|
|
slog.Info("analytics: posthog client enabled", "host", host)
|
|
return NewPostHogClient(PostHogConfig{
|
|
APIKey: key,
|
|
Host: host,
|
|
Environment: EnvironmentFromEnv(),
|
|
})
|
|
}
|
|
|
|
func isDisabled() bool {
|
|
v := os.Getenv("ANALYTICS_DISABLED")
|
|
return v == "true" || v == "1"
|
|
}
|
|
|
|
func EnvironmentFromEnv() string {
|
|
if v := normalizeEnvironment(os.Getenv("ANALYTICS_ENVIRONMENT")); v != "" {
|
|
return v
|
|
}
|
|
if v := normalizeEnvironment(os.Getenv("APP_ENV")); v != "" {
|
|
return v
|
|
}
|
|
return "dev"
|
|
}
|
|
|
|
func normalizeEnvironment(v string) string {
|
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
|
case "production", "prod":
|
|
return "production"
|
|
case "staging", "stage":
|
|
return "staging"
|
|
case "development", "dev", "test", "local":
|
|
return "dev"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// NoopClient silently drops all events. Used in tests, in local dev when
|
|
// POSTHOG_API_KEY is unset, and in self-hosted instances that opt out.
|
|
type NoopClient struct{}
|
|
|
|
func (NoopClient) Capture(Event) {}
|
|
func (NoopClient) Close() {}
|