Skip to content

feat(analytics): emit PostHog product-analytics events#797

Draft
EDsCODE wants to merge 1 commit into
mainfrom
feat/posthog-event-tracking
Draft

feat(analytics): emit PostHog product-analytics events#797
EDsCODE wants to merge 1 commit into
mainfrom
feat/posthog-event-tracking

Conversation

@EDsCODE

@EDsCODE EDsCODE commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds per-org product-analytics event tracking to PostHog, gated on the existing POSTHOG_API_KEY. This is separate from the OTLP log export (which ships slog records to PostHog Logs) — these are discrete product events you can build insights and dashboards on. When POSTHOG_API_KEY is unset, nothing is sent.

Events are attributed to an org via PostHog group analytics: distinct_id = org, group type organization. Standalone (no org) → distinct_id = "standalone", no group. No SQL text, credentials, or secret values are ever sent — only metadata.

Events

Event Fires when Properties
warehouse_provisioned Managed warehouse provisioned (admin API) database_name, metadata_store, ducklake_enabled, iceberg_enabled
warehouse_deprovisioned Warehouse deprovisioned (admin API)
warehouse_password_reset Org root password reset (admin API) username
query_initiated Client query begins execution user, trace_id
query_failed Query errors user, trace_id, error_code (SQLSTATE), error_category (user/system/conflict/metadata_connection_lost)

Changes

  • New internal/analytics package: Tracker interface (Capture/Close) over posthog-go, with a no-op default and a global installed via SetDefault (mirrors the slog.SetDefault pattern so call sites are unconditional). Capture is async/batched, so it never blocks the query path.
  • cliboot.InitAnalytics: reads the same POSTHOG_API_KEY/POSTHOG_HOST as log export; installs the real tracker when the key is set, returns a flush-on-shutdown. Wired into main.go (standalone + process control-plane) and cmd/duckgres-controlplane/main.go (remote/k8s). The child-worker path is intentionally left untouched (no client connections / admin API there).
  • Admin events in controlplane/provisioning/api.go, emitted only on the success path of each handler.
  • Query events in server/conn.go: query_initiated in logQueryStarted, query_failed in logQueryError (reusing the existing severity classification for error_category).
  • Docs: README env table + new "PostHog Product-Analytics Events" section (including the no-sampling volume note); tests/e2e-mw-dev/README.md documents why PostHog ingestion can't be asserted in-Job and points at the unit coverage.

Design notes

  • Per-query, no sampling (per request): query_initiated volume scales 1:1 with query throughput. Async batched capture keeps it off the latency path; if volume bites later, the Tracker interface makes sampling a localized change.
  • Query events run in the control plane for the remote backend (it owns client connections, PG wire, and org identity), which is exactly where POSTHOG_API_KEY lives.

Testing

  • internal/analytics/analytics_test.go — group-analytics mapping, no-org fallback, default/no-op behavior.
  • controlplane/provisioning/analytics_events_test.go — each admin event with properties, and no event emitted on handler failure.
  • server/conn_analytics_test.go — query events + error-category classification.
  • Builds verified on default and kubernetes tags.

Rollback

  • Revert this PR. With POSTHOG_API_KEY unset the tracker is a no-op, so there is no runtime impact when the key is absent.

🤖 Generated with Claude Code

Add per-org product-analytics event tracking to PostHog, gated on the
existing POSTHOG_API_KEY (separate from the OTLP log export). Events use
PostHog group analytics keyed on the "organization" group type.

Events:
- warehouse_provisioned / warehouse_deprovisioned / warehouse_password_reset
  (admin provisioning API success paths)
- query_initiated (logQueryStarted) and query_failed (logQueryError), the
  latter carrying SQLSTATE + a user/system/conflict error category

New internal/analytics package wraps posthog-go behind a Tracker interface
with a no-op default; cliboot.InitAnalytics installs the real tracker when
POSTHOG_API_KEY is set and is wired into the standalone and control-plane
entrypoints. Capture is async/batched so it stays off the query latency path.
No SQL text or secrets are ever sent — only metadata.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant