User profiles, guest IDs, admin and QA classification, and PostHog filter setup
Email and username are intentional person properties, not general event metadata. Only verified Auth responses and the secret-protected Auth-to-App ingest may supply them. Neither a browser event nor an account selector can assert admin status or change the actor's identity.
| Person property | Source and meaning |
|---|---|
user_id, zooly_user_id | Canonical Zooly identityId, falling back to the authenticated ID. Also used as PostHog distinct_id. Never an email. |
email | Auth/Cognito email; a real guest identity may supply its stored guest email. Unavailable email is null, not fabricated. |
username | Zooly display name. Zooly does not currently have a separate user handle. Account slug remains account_slug. |
guest_id | Original Auth guest identity ID, retained after linking; otherwise a domain-separated SHA-256 pseudonym of the anonymous room-access cookie, when one exists. |
is_guest | True for an actual guest identity; false after authentication/linking. Anonymous browsing does not by itself create a person profile. |
is_admin | Verified Auth admin role. |
identity_is_zqa | Auth zQA role or isQaEmail: any @zooly.ai address, a name+tag@gmail.com alias, or a UUID-local-part QA inbox. Uses the verified Auth email, never browser-supplied traits. |
account_is_zqa | The actor's own, server-resolved account has data_tag = 'zQA'. |
is_zqa | identity_is_zqa OR account_is_zqa. |
internal_user | is_admin OR is_zqa. |
The room-access cookie is a credential, not an analytics ID. Its raw value is never sent to PostHog. A guest pseudonym represents browser/room continuity, not proof of the same human on a shared device. It is never used to merge identified people. A visitor with no guest cookie/identity simply has PostHog's anonymous ID and a journey session ID.
$process_person_profile: false until a canonical user is known./api/me verifies the Auth session. App's /api/analytics/identity additionally resolves the actor's own account tag and guest pseudonym. These responses use private, no-store.posthog.identify(canonicalId, properties) connects the anonymous browser to the authenticated person. Email signup, social redirects and the modal/popup completion paths refresh identity.Auth alone does not know the account tag and therefore does not overwrite account_is_zqa, is_zqa or internal_user. App owns those combined flags. Existing guest-to-account linking preserves the canonical Auth ID; do not add arbitrary aliases from query parameters.
Anonymous replay can start immediately under the existing consent policy. If the browser persists an identified person, replay and the shared analytics context wait for identity verification to avoid attributing a different user's initial page to that person.
A real person opening a zQA Z-Link stays a real person. The target's classification is recorded separately as target_account_is_zqa; it never sets that visitor's person-level internal_user. Account and agency groups also expose their own is_zqa property; group tags are not person tags.
Events expose actor/target account dimensions and data_tag. A trusted analytics_identity envelope stores non-PII event-time flags in the first-party database. The envelope is stripped before PostHog capture and only its allowlisted fields are flattened. Email and username are not stored in this envelope.
Fresh authenticated deliveries can update PostHog person properties through $set. Backend-only events and retries resolve the current profile through Auth's server-only /api/internal/analytics/identity endpoint, using the existing environment-specific journey-ingest secret. This covers real guest identities even without a browser login. The lookup has a three-second timeout and fails open for event delivery, not for identity enrichment: an outage leaves unknown properties unset.
Retried historical rows do not overwrite current person properties with old role/tag values. They may refresh traits only from the trusted live lookup; otherwise they set stable user IDs only. Opening/focusing App also retries enrichment. There is no full historical backfill or scan of all users.
These are project settings, not settings automatically changed by merging code. First deploy App/Auth and validate a fresh QA, admin and real-user session in the correct project. The property names become discoverable after ingestion. Check a person's profile, not only the Event properties dropdown.
Following PostHog's internal-user filtering guide:
internal_user is not true, explicitly retaining people for whom the property is not set. If the UI's boolean negative operator does not include unset values, use an OR group: internal_user = false OR internal_user is not set.is_zqa = true. Use it for QA-only analysis. Keep admins (is_admin = true) separately inspectable.is_zqa = true or the cohort. A product-analytics toggle is not a universal filter for every Replay or SQL view.environment = production for Production reporting; use development for Dev. Do not remove the Dev domain or rotate tokens for this feature.Email is available as a person property after identification and can support an additional internal-domain filter. Do not exclude all @zooly.ai users unless that is the team's intended policy. Prefer the explicit role/tag flags for the primary rule.
Turning off the internal/test-user filter includes all people. SQL insights must apply their own predicates. Account-level QA analysis should use target/account dimensions, not mark all visitors as QA people.
Stats remains Production-only and admin-only. Its QA data control is independent of PostHog settings:
Both summary and recent journeys use the same predicate. QA membership uses stored event tags/flags or current, non-deleted actor/target/owner accounts. Earlier events are connected by user_session_id within the same environment. Historical unlinked anonymous sessions cannot be classified retroactively.
Account search uses the selected QA mode. A real agency with QA roster members remains selectable under Only zQA; agency classification uses active relationships, not the filtered member count. Deleted accounts remain excluded in every mode. Changing QA mode clears the current account selection and discards stale requests.
Admins are also assigned zQA, so Stats' Exclude zQA drops their tagged journeys after the account stamp lands. Person properties represent the latest known identity; event flags represent event-time state, and Stats also checks current account tags. These views need not produce identical historical counts.
The App identity refresh reconciles the signed-in user's own account tag
from verified QA roles/email. This makes earlier events linked to that account
filterable without rewriting historical event payloads. It never tags another
account selected through Acting As, and never infers QA from a display name
containing test or qa. Auth login also assigns zQA to every admin, every
@zooly.ai address, and Gmail plus-aliases. Legacy accounts not revisited
still need a verified identity reconciliation; this is not an automatic
database-wide backfill.
guest_id is conditional: it is sent when there is a canonical Auth guest
identity or a safe digest of the App's guest cookie. A direct authenticated
login without a guest identity need not have it. To validate guest-to-login,
start in a fresh anonymous browser, enter the guest flow, then sign in in that
same browser. Confirm the guest ID on the resulting person properties and
events. Merely opening /api/analytics/identity reads the payload; it does not
send an identify event to PostHog.
The native PostHog internal-user filter is an inclusion condition: keep person
internal_user != true (including unset properties). Validate with the QA
person present when the toggle is off and absent when it is on. Enable the
default for new insights; existing saved insights keep their own toggle until
explicitly changed. This does not delete events or recordings.
The App identity refresh reconciles the signed-in user's own account tag
from verified QA roles/email. This makes earlier events linked to that account
filterable without rewriting historical event payloads. It never tags another
account selected through Acting As, and never infers QA from a display name
containing test or qa. It does not change Auth roles or grant/revoke admin.
An ordinary name@zooly.ai administrator remains non-QA unless their account
is explicitly tagged. Legacy accounts not revisited still need a verified
identity reconciliation; this is not an automatic database-wide backfill.
guest_id is conditional: it is sent when there is a canonical Auth guest
identity or a safe digest of the App's guest cookie. A direct authenticated
login without a guest identity need not have it. To validate guest-to-login,
start in a fresh anonymous browser, enter the guest flow, then sign in in that
same browser. Confirm the guest ID on the resulting person properties and
events. Merely opening /api/analytics/identity reads the payload; it does not
send an identify event to PostHog.
The native PostHog internal-user filter is an inclusion condition: keep person
internal_user != true (including unset properties). Validate with the QA
person present when the toggle is off and absent when it is on. Enable the
default for new insights; existing saved insights keep their own toggle until
explicitly changed. This does not delete events or recordings.
Run npm ci then npm run test:product-analytics. The suite covers trusted profile allowlisting, guest pseudonyms, browser login/logout races, server retries and real PostgreSQL queries (PGlite) for the three QA modes and agency scopes.
Deploy App before Auth's enriched internal events, then Auth, Stats and Docs. The internal schema is backward-compatible with old Auth senders; old App receivers reject the new analyticsUser field, so rollout order matters. No new environment variable, token, production migration or existing-data deletion is required.
After Dev validation, promote the reviewed change to Stage/Main with target-specific conflict checks. Production end-to-end acceptance must use a new app.zooly.ai session. PR creation, deployment and native PostHog filter activation are separate checkpoints, not proof of one another.
Before declaring complete, verify: one real person; one zQA person; one admin; a real person visiting a QA target; guest-to-signup; email and social signup; modal and Z-Link; logout then a second user; role/tag removal; all three Stats modes; and QA/real Replay filters. Do not claim historical replays or missing identity data have been backfilled.