track, identify mapped to PostHog’s alias API, and how that ties to session-backed anonymousAnalyticsId and login flows.
Why
PostHog gives you product analytics with flexible deployment options and a straightforward server SDK. If you need event tracking with provider-level host configurability while keeping the existing analytics abstraction, PostHog fits naturally into the pluggable tooling architecture.How it works
Dual identity ontrack: Every track call receives AnalyticsIdentificationAttributesType: { anonymousId: string; userId?: string }. The adapter uses primary distinct id userId ?? anonymousId for capture.
identify: The shared contract exposes identify({ anonymousId, userId }). The PostHog adapter maps this to posthog.alias({ distinctId: userId, alias: anonymousId }), linking the anonymous distinct id to the authenticated user id (see PostHog docs for alias semantics).
When identify runs (server-only):
- After successful login (
signInWithPassword,completeInvitedProfile, OAuth callback) viaidentifyUserInAnalytics. - Legacy repair: middleware runs
ensureAnonymousAnalyticsIdthensyncAnalyticsIdentityIfNeeded— if a new anonymous id was just created (wasCreated) and the user is already authenticated,identifyruns on that request so pre-deploy sessions don’t stay unlinked.
tools.analytics and GraphQL trackFrontendEvent.
Setup
- Configure PostHog in
apps/backend/src/configuration/production.ts(orstaging.ts) with a server-side key:settingsis optional in the shared type; if omitted, the SDK uses its defaults (you still needapiKeyandtrackingPolicy). - Set env vars in your deployment:
ANALYTICS_APPis required whenever analytics is enabled. The backend attaches it as theappproperty on every outboundtrackevent so you can filter multiple Refract deployments in one PostHog project. - Run verification:
Tracking policy behavior
trackingPolicy.allowedFields is top-level only in v1. Any field not listed is dropped. Fields with pii: true are masked before being sent to PostHog. Policy logs include field names only and never raw field values.
Frontend tracking endpoint
Frontend clients should send product analytics through the backend GraphQL mutationtrackFrontendEvent instead of calling provider SDKs directly. This keeps validation, rate limiting, and policy enforcement centralized.
eventDomain is constrained, eventName must match a namespaced format (domain.action), and properties are key/value pairs with backend validation limits. The resolver resolves { anonymousId, userId? } from the session and request.
Future: browser SDK and one canonical id
When you add a browser (PostHog JS) or other client SDK, the intended pattern is to surface the sameanonymousAnalyticsId the backend already stores in session — for example a first-party cookie set on responses that mirrors req.session.anonymousAnalyticsId — so the client initializes with the same distinct id, not a second generator. Choosing HttpOnly vs JS-readable and SameSite is a product/security decision for that phase.
Extending this system
- Adjust capture or alias behavior in
apps/tools/analytics/posthog/src/index.ts. - If you change the shared contract, update
apps/shared/src/analytics/types.tsandapps/shared/src/analytics/schema.ts. - Run
make test module=tooling-analytics-posthogandmake test module=backend path=apps/backend/src/tools/analytics/__tests__/loader.spec.ts.
What not to do
❌ Never import PostHog directly in a GraphQL resolver or React component. Usetools.analyticsfrom the backend. ❌ Never use high-cardinality values as tags in telemetry; follow.cursor/rules/security.mdcfor analytics properties. ❌ Never configureAnalyticsClientType.LOCALor dev keys in production — use PostHog only in prod/staging configs.
File conventions
Gotchas
POSTHOG_API_KEYmust be a project API key from server-side environment config, not a personal API key.- If
trackingPolicy.allowedFieldsdoes not include a field, the field is removed before capture. - Local analytics can run without strict policy enforcement, so local logs may differ from production payloads.
What’s next?
- Amplitude — another production analytics provider with the same interface.
- Local — dev/test analytics stub.
- Configuration — selecting tool implementations by environment.
