PostHog
Our product analytics (posthog-js + posthog-node).
Environment
VITE_POSTHOG_KEY= # project API key — public by design
VITE_POSTHOG_HOST= # https://us.i.posthog.com
The project key is meant to be public — it can only write events. It is not a secret, but it is a spam vector, so keep the ingestion endpoint behind a reverse proxy if event volume ever becomes a cost problem.
Client
posthog.init(import.meta.env.VITE_POSTHOG_KEY, {
api_host: import.meta.env.VITE_POSTHOG_HOST,
person_profiles: "identified_only", // don't bill for anonymous profiles
capture_pageview: true,
});
Server
import { PostHog } from "posthog-node";
const posthog = new PostHog(key, { host });
posthog.capture({ distinctId: userId, event: "loan_requested", properties: { amount_usdc: 5000 } });
await posthog.shutdown(); // serverless: REQUIRED, or events are lost
await posthog.shutdown() before a serverless function returns. posthog-node
batches, and the function freezes with the batch unsent otherwise. This is the single
most common reason server events "don't appear".
Identifying
posthog.identify(userId, { email: user.email }); // on login
posthog.reset(); // on logout — or the next user inherits the session
Use a stable internal id as distinctId. Not a wallet address (users have
several, and they rotate), not an email (it changes).
What not to capture
In a portfolio that touches lending, real-estate purchases and payments, this matters more than the tracking itself:
- ❌ Private keys, seed phrases, session tokens, API keys — obviously
- ❌ Full wallet addresses as properties. They are a permanent pseudonymous identifier that links a person to their entire onchain history. Hash them, or capture only that a wallet exists.
- ❌ Identity documents, KYC fields, credit-score inputs
- ❌ Exact loan amounts tied to an identifiable individual — bucket them
- ✅ Event names, funnel steps, bucketed values, feature-flag exposure
Enable session recording only with masking on, and never on a KYC or payment form.
Feature flags
if (await posthog.isFeatureEnabled("new-auction-ui", userId)) { … }
Server-side flag checks are a network call — cache per request, and always have a default for when PostHog is unreachable. A flag service outage must not take the product down.
Gotchas
- Missing
shutdown()in serverless → lost events. - No
reset()on logout → merged user identities, permanently. - Wallet addresses as properties → de-anonymisation.
person_profiles: "always"bills for every anonymous visitor.- Flag check with no fallback couples your uptime to theirs.