Analytics privacy posture — Veqtrx

Analytics privacy posture

What PostHog sees, what it doesn’t, and how a user can opt out.

Principle

PostHog collects anonymous behavioural events. It does not see PII. The application database (Postgres / SQLite) is the only place that holds any volunteered email or customer data. The two stores are deliberately decoupled — a SQL drop on demo_sessions removes every captured email; PostHog still has the (anonymous) funnel.

What PostHog gets

✅ Event names (homepage_viewed, cta_clicked, …) ✅ Anonymous user identifiers (UUID generated by posthog-js, held in persistence: 'memory' — tab-scoped, no cookie, no localStorage, regenerates on page reload. Switched to memory persistence on 2026-05-18 so the privacy policy can truthfully claim “essential cookies only” and no PECR consent banner is required. Trade-off: uniques are slightly inflated because a page reload counts as a new anonymous visitor.) ✅ Non-PII properties (e.g. cta_name, archetype_id, step_name, transaction_count) ✅ Page paths (/demo, /demo/abc-123/circumstances) ✅ Click coordinates / element selectors (only if autocapture is on — currently off in instrumentation-client.ts) ✅ Time-on-page, scroll depth bucketed to 25/50/75/100% ✅ Browser / device type / OS

What PostHog NEVER gets

❌ Full email addresses — we send email_domain (“gmail.com”) only, computed via emailDomain() (front) / email_domain() (back). ❌ Names (first, last, full) ❌ Phone numbers ❌ Postal addresses or postcodes ❌ Financial amounts (income, transaction value, balance) ❌ Transaction descriptions / merchant strings of the customer’s bank ❌ IP addresses — the SDK is configured with ip: false

Defence in depth

Three layers of stripping:

  1. Application code. Backend analytics.track() (and frontend track()) refuses to forward properties whose names look like PII.
  2. SDK config. instrumentation-client.ts sets a sanitize_properties callback + a property_denylist so anything the application accidentally passes is filtered before the network call.
  3. PostHog project settings. Server-side property filters mirror the above. (Manual step — operator sets this in the PostHog UI.)

If any one layer fails, the next catches the leak.

PostHog Cloud is GDPR-compliant when configured to send anonymous events. The current configuration:

User opt-out

Two paths:

  1. Programmatic. Call optOut() from app/lib/analytics.ts. Wired by the cookie banner’s “Reject” button (TODO — see below). Persists in localStorage under the key posthog_optout. The loaded callback in instrumentation-client.ts checks this on every page load and calls posthog.opt_out_capturing() if set.

  2. Browser-level. Visitors with Do Not Track or third-party-cookie blockers will simply not load posthog-js scripts at all (Netlify serves posthog-js from PostHog Cloud’s edge, which respects these signals).

Not yet implemented. The spec called for a simple one-line banner with a Reject button. To stay within the launch time budget we deferred it. Today the privacy story is:

Pre-launch action item: add the footer note. Week 1 post-launch action item: add the cookie banner with Reject wired to optOut().

Cross-reference: backend telemetry

PostHog is for behavioural analytics. The structured logs from back/logger.py (e.g. voting_decided, parallel_executor_run, engine_routed) are sent via structlog to stdout (and to Render’s log aggregator in prod). Those logs CAN contain merchant strings + amounts because they live behind authentication on a private infrastructure endpoint. The bright line is:

Store PII allowed? Authentication?
PostHog Cloud ❌ Never None (anonymous events)
Render logs / Sentry ⚠️ Sparingly — only when needed for debugging Yes (internal access)
Application DB ✅ Yes (encrypted at rest in prod) Yes

Right to be forgotten

A user who wants their data removed needs both:

  1. Application DB delete. Currently no self-service endpoint for demo users (see docs/email-capture-guide.md known limitations). Manual SQL via the admin operator works today.
  2. PostHog distinct_id reset. Either:

PostHog itself supports a per-user delete via their dashboard or API if a user provides their distinct_id.

What to do before a privacy review

If a customer / press / regulator asks for the privacy posture:

  1. Point at this doc.
  2. Confirm in the PostHog project settings:
  3. Show the source: the three defence-in-depth layers above are all readable in the public repo.
  4. The cookie banner is the one outstanding UX gap; the analytics implementation is GDPR-aligned today.