PostHog
Product analytics with feature flags and session replay, on both client and server.
PostHog is an open-source product analytics platform that also offers feature flags and session replay. It supports both browser and backend tracking, so the same trakoo event definitions work on the client and the server.
When to use it
- You want product analytics that span the browser and your backend.
- You need a single tool for events, funnels, feature flags, and session replay.
- You want to self-host or keep data in a specific region.
If you only need lightweight page views, a privacy-friendly option like Pirsch or Visitors may be a better fit.
Installation
The PostHog providers ship in the @trakoo/posthog package. Install it alongside trakoo and only the PostHog SDK for the side you use — posthog-js for the client, posthog-node for the server.
# Client
pnpm add trakoo @trakoo/posthog posthog-js
# Server
pnpm add trakoo @trakoo/posthog posthog-node
Client-side usage
Use PostHogClientProvider from @trakoo/posthog/client. It accepts every option from the PostHog JS SDK, so anything you can pass to posthog.init() works here.
import { createClientAnalytics } from "trakoo/client";
import { PostHogClientProvider } from "@trakoo/posthog/client";
import { appEvents } from "@/lib/events";
export const analytics = createClientAnalytics({
events: appEvents,
providers: [
new PostHogClientProvider({
token: import.meta.env.VITE_POSTHOG_KEY,
api_host: import.meta.env.VITE_POSTHOG_HOST,
}),
],
});
Browser instance isolation
Every PostHogClientProvider uses its own named PostHog SDK object by default.
That keeps each provider’s live configuration and calls to identify,
capture, and reset isolated from other Trakoo instances.
Most applications should omit instanceName. It is an advanced escape hatch
for deliberately selecting a PostHog named instance:
new PostHogClientProvider({
token: import.meta.env.VITE_POSTHOG_KEY,
instanceName: "marketing",
});
Reusing the same instanceName opts providers into sharing that named SDK
object. The instance name does not change PostHog’s persistence semantics:
stored identity continues to use the project token by default, or
persistence_name when you configure one. Generated instance names therefore
do not make browser identity disposable across normal navigation or reloads.
Page views
By default the PostHog JS SDK captures $pageview and $pageleave on its own.
If you also call trakoo’s pageView() or pageLeave(), PostHog counts each
view twice. Choose one source. Either turn off PostHog’s automatic page views,
which also turns off its automatic page leaves:
new PostHogClientProvider({
token: import.meta.env.VITE_POSTHOG_KEY,
capture_pageview: false,
});
Or keep PostHog’s automatic capture and exclude trakoo’s page calls from this provider with routing:
{
provider: new PostHogClientProvider({ token: import.meta.env.VITE_POSTHOG_KEY }),
exclude: ["pageView", "pageLeave"],
}
Events from track() keep the $current_url that the SDK reads from the
browser’s location at capture time. trakoo’s page context is not used for it.
Server-side usage
Use PostHogServerProvider from @trakoo/posthog/server. Server analytics is stateless, so pass user context with each call.
import { createServerAnalytics } from "trakoo/server";
import { PostHogServerProvider } from "@trakoo/posthog/server";
import { appEvents } from "@/lib/events";
export function createRequestAnalytics() {
return createServerAnalytics({
events: appEvents,
providers: [
new PostHogServerProvider({
apiKey: process.env.POSTHOG_API_KEY!,
host: "https://us.i.posthog.com",
}),
],
});
}
import { createRequestAnalytics } from "@/lib/server-analytics";
export async function POST() {
const analytics = createRequestAnalytics();
try {
await analytics.track(
"user_signed_up",
{
plan: "pro",
},
{
userId: "user_123",
},
);
return Response.json({ ok: true });
} finally {
await analytics.shutdown();
}
}
Identity
Server identity is never retained between calls. Each event takes its distinct ID from that call alone:
track()uses theuserIdoption, thencontext.user.userId.pageView()usescontext.user.userId, then the email supplied in that call.
An event with neither is sent anonymously. It gets a random distinct ID and
$process_person_profile: false, so PostHog records the event without
creating a person profile, and anonymous visitors are never merged into one
person. Include user context on every call that should be attributed to a user.
Request context
A server event comes from your server, not from the visitor. Pass the visitor’s details in the event context, and the provider maps them to PostHog’s own properties:
await analytics.track("user_signed_up", { plan: "pro" }, {
userId: "user_123",
context: {
page: { path: "/signup", url: request.url },
server: {
ip: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
userAgent: request.headers.get("user-agent") ?? undefined,
},
},
});
| trakoo context | PostHog property |
|---|---|
server.ip, or device.ip if unset |
$ip |
server.userAgent, or device.userAgent if unset |
$raw_user_agent |
page.url, or page.path if unset |
$current_url |
utm.source, utm.medium, utm.name |
utm_source, utm_medium, utm_campaign |
The Node SDK turns GeoIP off by default, because it would locate your server.
When an event carries a visitor IP, the provider turns GeoIP on for that event
so PostHog locates the visitor. Set disableGeoip: true to keep it off. The IP
is sent only as $ip, which PostHog’s setting to discard client IP data
covers. A device.ip is removed from the forwarded device property.
Resolve the visitor’s address from a source your platform vouches for, such as
x-forwarded-for behind a trusted proxy. A header you do not control lets the
caller claim any location.
trakoo’s event time becomes the PostHog event timestamp.
Configuration
PostHogClientProvider forwards all PostHog JS options. The keys you’ll set most often:
tokenstring
Your PostHog project API key.
stringapi_host?string
PostHog instance URL. Change this for EU or self-hosted.
stringhttps://us.i.posthog.comdebug?boolean
Log PostHog SDK activity to the console.
booleanfalseenabled?boolean
Set to false to disable the provider without removing it.
booleantrueinstanceName?string
Advanced: select a named PostHog SDK instance. Omit for an isolated Trakoo-managed instance.
stringPostHogServerProvider forwards all PostHog Node options. The common ones:
apiKeystring
Your PostHog project API key.
stringhost?string
PostHog instance URL. Change this for EU or self-hosted.
stringhttps://us.i.posthog.comflushAt?number
Number of queued events that triggers a flush.
number20flushInterval?number
Milliseconds to wait before flushing queued events.
number10000disableGeoip?boolean
Unset: GeoIP runs only for events that carry a visitor IP. true: GeoIP never runs. false: GeoIP runs for every event, and events without a visitor IP are located at your server.
boolean