Database-first analytics, PostHog delivery, environment routing, and identity stitching
The dispatcher persists a normalized event before attempting PostHog delivery:
journey_event.posthog_delivered_at; failure records an attempt and safe error text.$insert_id uses the first-party event ID so PostHog can deduplicate repeated attempts.Email delivery events stay in the first-party store for audit and aggregate reporting, but are marked delivered without being copied to PostHog. This keeps Activity focused on product behavior instead of notification noise.
The browser creates a one-hour user_session_id in local storage. It also exposes the current PostHog distinctId and $session_id to the journey logger.
PostHog receives the first available stable identity in this order:
user_session_idThe Stats API groups rows by user_session_id first. This lets activity that began anonymously remain in the same journey after signup associates later events with a user or account.
Email is never used as distinctId. The stable internal identity ID is used
after authentication.
Verified email, username and admin/zQA flags are now allowlisted person properties. App resolves its own account tag separately from the target account. Login refreshes browser identity; successful logout resets both the SDK and journey ID. See Identity and zQA Filters for the guest-ID model, trusted enrichment channel and retry boundaries.
Account fields have different meanings:
account_id / actor_account_id: the authenticated actor's account, resolved by the serverentity_id / target_account_id: the account or entity being acted onagency_ids and agency_slugs: active agency relationships derived from account_agentsThe browser can describe a target through entityId, but it cannot choose its actor account. This prevents a client from attributing activity to an arbitrary account.
PostHog receives both flat properties and $groups for account and agency. Flat fields remain usable when Group Analytics is unavailable on the current PostHog plan.
zooly-auth is shared by Dev and Production, so it cannot infer the destination from its own deployment alone. The App passes a validated journey context containing:
user_session_idAuth accepts only known origins and selects the matching signed ingest target:
dev.zooly.ai → Development ingestapp.zooly.ai → Production ingestThe internal endpoint validates Authorization: Bearer <secret> using a timing-safe comparison. Arbitrary return URLs cannot select an ingest destination.