Analytics
Events from the browser and from the server, sent to PostHog and tied to the signed-in user.
@zap-ts/analytics lets the app record what people do. It sends these events
to PostHog. PostHog is a product analytics service.
The package has two export paths. @zap-ts/analytics/client is for the
browser. It gives you one React component to wrap your app in.
@zap-ts/analytics/server is for events the server sends itself. Both talk to
the same PostHog project, and both read the same two variables.
The browser side
AnalyticsProvider wraps the app. Both apps/web and apps/marketing use it
in their root route.
import { AnalyticsProvider } from "@zap-ts/analytics/client";
const App = () => (
<AnalyticsProvider>
<Routes />
</AnalyticsProvider>
);
Inside, it is the PostHogProvider of @posthog/react. The package sets
person_profiles: "identified_only". So PostHog keeps a profile for people you
name, and not for every visitor who passes by.
Is POSTHOG_KEY missing? Then the provider just returns its children, and
sends nothing. A fresh copy of the project still runs, and no component has to
check whether analytics is set up.
Why it needs the authentication package
The provider does one more thing. It keeps the PostHog identity equal to the
signed-in user. That is the only reason @zap-ts/analytics depends on
@zap-ts/authentication.
A small component inside the provider, IdentifyUser, reads the session. It
uses authClient.useSession() from @zap-ts/authentication/client. A session
is the record that says who is signed in right now. Then it does one of two
things:
- Someone is signed in. It calls
posthog.identify(userId). Now events belong to that person. - Nobody is signed in. It calls
posthog.reset(). So the next person on that browser does not get the events of the last one.
So your sign-in and sign-out code needs no analytics call. The session changes, and the identity follows by itself.
After the provider is in place, components send events with usePostHog from
@posthog/react. The package does not wrap that hook.
The server side
The server side is an Effect service. ServerAnalytics is a tag. A tag is a
name that stands for a service you provide later. This one holds a
posthog-node client. ServerAnalyticsLive is the layer that builds it. A
layer is the recipe that makes a service real.
import { capture, ServerAnalyticsLive } from "@zap-ts/analytics/server";
import { Effect } from "effect";
const program = capture({
distinctId: "user_1",
event: "subscription_started",
properties: { plan: "team" },
});
await Effect.runPromise(program.pipe(Effect.provide(ServerAnalyticsLive)));
capture takes an EventMessage from posthog-node. It needs at least two
fields: distinctId, which says who did the thing, and event, which says
what they did. You can add properties for anything else you want to keep.
Use the same user id on the server and in the browser. PostHog joins the two sets of events by that id.
The layer is scoped. That means it cleans up after itself. It builds the client
at the start, and it waits for client.shutdown() at the end. That call sends
anything still waiting in the queue. This matters when the process stops soon
after the request, because a later background send would never happen.
The layer also sets flushAt: 1 and flushInterval: 0. So each event leaves
right away. It does not wait for a group of events.
The server reads its key with requireEnv, and the browser does not. So a
missing key is quiet in the browser and an error on the server. A server event
is usually one you really meant to send.
Environment
POSTHOG_KEY=
POSTHOG_HOST=https://eu.i.posthog.com
The root .env.schema file declares both. Both are marked as not sensitive,
because the browser gets the key anyway.
POSTHOG_KEY must start with phc_, and it is optional. POSTHOG_HOST is
required. It points at the EU cloud by default. Is your project in the US? Then
use https://us.i.posthog.com. You can also point it at your own proxy.
See Environment to learn how varlock turns those
declarations into the typed ENV that the code imports.
Related
- Authentication — the session the provider reads
- Flags — feature flags on the same PostHog project
- Environment —
envandrequireEnv