Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions context/commandments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ commandments:
# Server-side feature flags
- Server Components and Route Handlers cannot use React hooks - use posthog-node SDK instead
- Create a server-side PostHog client with posthog-node, call getAllFlags() or getFeatureFlag(), then await posthog.shutdown()
- 'Route Handlers and Server Actions are short-lived per request. Create the posthog-node client with flushAt 1 and flushInterval 0, and `await posthog.flush()` after capturing and before returning, or the function freezes before the event is sent and it is silently dropped'
- Pass flag values from server to client components as props to avoid hydration mismatches
# Avoiding flicker
- For flags that affect initial render, evaluate server-side and pass as props to prevent UI flicker
Expand All @@ -56,8 +57,8 @@ commandments:
- 'Include enableExceptionAutocapture: true in the PostHog constructor options'
- Add posthog.capture() calls in route handlers for meaningful user actions – every route that creates, updates, or deletes data should track an event with contextual properties
- Add posthog.captureException(err, distinctId) in the application's error handler (e.g., Express error middleware, Fastify setErrorHandler, Koa app.on('error'))
- In long-running servers, the SDK batches events automatically – do NOT set flushAt or flushInterval unless you have a specific reason to
- For short-lived processes (scripts, CLIs, serverless), set flushAt to 1 and flushInterval to 0 to send events immediately
- The SDK batches events and flushes asynchronously. await flush() or await shutdown() before letting that process exit. If unsure, set flushAt 1 and flushInterval 0.
- '`posthog.capture()` enqueues synchronously and returns; the batched HTTP send happens afterwards. Treat every per-request handler as short-lived even when the framework feels like a server: Next.js / Nuxt / SvelteKit / Remix route handlers, serverless and edge functions, and Lambda are torn down per invocation before the send runs. Create the client with flushAt 1 and flushInterval 0, then await the send before returning. Always use `await posthog.flush()` for a shared/singleton client, `await posthog.shutdown()` for a per-request client. Never skip the awaited flush or risk the enqueued event being silently dropped.'
- Reverse proxy is NOT needed for server-side Node.js – only client-side JavaScript needs a proxy to avoid ad blockers

python:
Expand Down Expand Up @@ -195,6 +196,7 @@ commandments:
- Test queries in the PostHog SQL editor before using them in insights or the API

sveltekit:
- 'For server-side capture (+server.ts endpoints, actions, hooks like handleError), the handler is short-lived per request. Configure the posthog-node singleton with flushAt 1 and flushInterval 0, and `await posthog.flush()` after capturing and before returning, or the batched event is silently dropped when the request ends'
- Set paths.relative to false in svelte.config.js — this is required for PostHog session replay to work correctly with SSR and is easy to miss
- Use the Svelte MCP server tools to check Svelte documentation (list-sections, get-documentation) and validate components (svelte-autofixer) — always run svelte-autofixer on new or modified .svelte files before finishing

Expand Down Expand Up @@ -254,11 +256,13 @@ commandments:
astro-ssr:
- Use posthog-node in API routes under src/pages/api/ for server-side event tracking
- Store the posthog-node client instance in a singleton pattern (src/lib/posthog-server.ts) to avoid creating multiple clients
- 'Configure the singleton with flushAt 1 and flushInterval 0, and `await posthog.flush()` in the API route after capturing and before returning the Response. An SSR endpoint is short-lived per request, so an unflushed batched event is silently dropped'
- Pass the client session ID to server via X-PostHog-Session-Id header for unified session tracking

astro-hybrid:
- Use posthog-node in API routes under src/pages/api/ for server-side event tracking
- Store the posthog-node client instance in a singleton pattern (src/lib/posthog-server.ts) to avoid creating multiple clients
- 'Configure the singleton with flushAt 1 and flushInterval 0, and `await posthog.flush()` in the API route after capturing and before returning the Response. An SSR endpoint is short-lived per request, so an unflushed batched event is silently dropped'
- In Astro 5, use output static (the default) with an adapter - pages are prerendered by default
- Use export const prerender = false to opt specific pages into SSR when they need server-side rendering
- Only pages that need server-side PostHog tracking (like API-backed forms) should opt out of prerendering
Expand Down
3 changes: 3 additions & 0 deletions example-apps/astro-hybrid/src/pages/api/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export const POST: APIRoute = async ({ request }) => {
},
});

// This endpoint is short-lived; flush so the enqueued events send before it returns
await posthog.flush();

return new Response(
JSON.stringify({
success: true,
Expand Down
3 changes: 3 additions & 0 deletions example-apps/astro-hybrid/src/pages/api/events/burrito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export const POST: APIRoute = async ({ request }) => {
},
});

// This endpoint is short-lived; flush so the enqueued event sends before it returns
await posthog.flush();

return new Response(
JSON.stringify({
success: true,
Expand Down
3 changes: 3 additions & 0 deletions example-apps/astro-ssr/src/pages/api/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export const POST: APIRoute = async ({ request }) => {
},
});

// This endpoint is short-lived; flush so the enqueued events send before it returns
await posthog.flush();

return new Response(
JSON.stringify({
success: true,
Expand Down
3 changes: 3 additions & 0 deletions example-apps/astro-ssr/src/pages/api/events/burrito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const POST: APIRoute = async ({ request }) => {
},
});

// This endpoint is short-lived; flush so the enqueued event sends before it returns
await posthog.flush();

return new Response(
JSON.stringify({
success: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ export async function POST(request: Request) {
}
});

// This handler is short-lived; flush so the enqueued events send before it returns
await posthog.flush();

return NextResponse.json({ success: true, user });
}
3 changes: 3 additions & 0 deletions example-apps/next-pages-router/src/pages/api/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@ export default async function handler(
}
});

// This handler is short-lived; flush so the enqueued events send before it returns
await posthog.flush();

return res.status(200).json({ success: true, user });
}
3 changes: 3 additions & 0 deletions example-apps/nuxt-4/server/api/auth/login.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default defineEventHandler(async (event) => {
},
})

// This handler is short-lived; flush so the enqueued event sends before it returns
await posthog.flush()

return {
success: true,
user,
Expand Down
3 changes: 3 additions & 0 deletions example-apps/nuxt-4/server/api/burrito/consider.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export default defineEventHandler(async (event) => {
},
})

// This handler is short-lived; flush so the enqueued event sends before it returns
await posthog.flush()

return {
success: true,
user: { ...user },
Expand Down
2 changes: 2 additions & 0 deletions example-apps/nuxt-4/server/utils/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export function useServerPostHog(): PostHog {
const posthogConfig = config.public.posthog
client = new PostHog(posthogConfig.publicKey, {
host: posthogConfig.host,
flushAt: 1,
flushInterval: 0,
})
}
return client
Expand Down
3 changes: 3 additions & 0 deletions example-apps/sveltekit/src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export const handleError: HandleServerError = async ({ error, status, message })
}
});

// handleError runs per request; flush so the enqueued event sends before it returns
await posthog.flush();

return {
message,
status
Expand Down
3 changes: 3 additions & 0 deletions example-apps/tanstack-start/src/routes/api/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export const Route = createFileRoute('/api/auth/login')({
},
})

// This handler is short-lived; flush so the enqueued events send before it returns
await posthog.flush()

return json({ success: true, user })
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export const Route = createFileRoute('/api/burrito/consider')({
},
})

// This handler is short-lived; flush so the enqueued event sends before it returns
await posthog.flush()

return json({ success: true })
},
},
Expand Down
Loading