diff --git a/context/commandments.yaml b/context/commandments.yaml index 4e847e0c..971780d5 100644 --- a/context/commandments.yaml +++ b/context/commandments.yaml @@ -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 @@ -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: @@ -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 @@ -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 diff --git a/example-apps/astro-hybrid/src/pages/api/auth/login.ts b/example-apps/astro-hybrid/src/pages/api/auth/login.ts index 32d04360..4d827219 100644 --- a/example-apps/astro-hybrid/src/pages/api/auth/login.ts +++ b/example-apps/astro-hybrid/src/pages/api/auth/login.ts @@ -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, diff --git a/example-apps/astro-hybrid/src/pages/api/events/burrito.ts b/example-apps/astro-hybrid/src/pages/api/events/burrito.ts index f413b26a..933e3ecf 100644 --- a/example-apps/astro-hybrid/src/pages/api/events/burrito.ts +++ b/example-apps/astro-hybrid/src/pages/api/events/burrito.ts @@ -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, diff --git a/example-apps/astro-ssr/src/pages/api/auth/login.ts b/example-apps/astro-ssr/src/pages/api/auth/login.ts index 13264834..f4c6ca02 100644 --- a/example-apps/astro-ssr/src/pages/api/auth/login.ts +++ b/example-apps/astro-ssr/src/pages/api/auth/login.ts @@ -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, diff --git a/example-apps/astro-ssr/src/pages/api/events/burrito.ts b/example-apps/astro-ssr/src/pages/api/events/burrito.ts index d62100f5..da8f31f0 100644 --- a/example-apps/astro-ssr/src/pages/api/events/burrito.ts +++ b/example-apps/astro-ssr/src/pages/api/events/burrito.ts @@ -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, diff --git a/example-apps/next-app-router/src/app/api/auth/login/route.ts b/example-apps/next-app-router/src/app/api/auth/login/route.ts index d328baac..d75f0d7b 100644 --- a/example-apps/next-app-router/src/app/api/auth/login/route.ts +++ b/example-apps/next-app-router/src/app/api/auth/login/route.ts @@ -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 }); } \ No newline at end of file diff --git a/example-apps/next-pages-router/src/pages/api/auth/login.ts b/example-apps/next-pages-router/src/pages/api/auth/login.ts index e67e7a60..2d86b29a 100644 --- a/example-apps/next-pages-router/src/pages/api/auth/login.ts +++ b/example-apps/next-pages-router/src/pages/api/auth/login.ts @@ -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 }); } diff --git a/example-apps/nuxt-4/server/api/auth/login.post.ts b/example-apps/nuxt-4/server/api/auth/login.post.ts index bf43cf7a..ef1cf96f 100644 --- a/example-apps/nuxt-4/server/api/auth/login.post.ts +++ b/example-apps/nuxt-4/server/api/auth/login.post.ts @@ -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, diff --git a/example-apps/nuxt-4/server/api/burrito/consider.post.ts b/example-apps/nuxt-4/server/api/burrito/consider.post.ts index cd039704..38d9fb66 100644 --- a/example-apps/nuxt-4/server/api/burrito/consider.post.ts +++ b/example-apps/nuxt-4/server/api/burrito/consider.post.ts @@ -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 }, diff --git a/example-apps/nuxt-4/server/utils/posthog.ts b/example-apps/nuxt-4/server/utils/posthog.ts index 8c23b750..e6446a92 100644 --- a/example-apps/nuxt-4/server/utils/posthog.ts +++ b/example-apps/nuxt-4/server/utils/posthog.ts @@ -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 diff --git a/example-apps/sveltekit/src/hooks.server.ts b/example-apps/sveltekit/src/hooks.server.ts index 8d25aaca..0e01a386 100644 --- a/example-apps/sveltekit/src/hooks.server.ts +++ b/example-apps/sveltekit/src/hooks.server.ts @@ -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 diff --git a/example-apps/tanstack-start/src/routes/api/auth/login.ts b/example-apps/tanstack-start/src/routes/api/auth/login.ts index 16b67a67..6f86fb02 100644 --- a/example-apps/tanstack-start/src/routes/api/auth/login.ts +++ b/example-apps/tanstack-start/src/routes/api/auth/login.ts @@ -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 }) }, }, diff --git a/example-apps/tanstack-start/src/routes/api/burrito/consider.ts b/example-apps/tanstack-start/src/routes/api/burrito/consider.ts index 31905902..8cc2b7d6 100644 --- a/example-apps/tanstack-start/src/routes/api/burrito/consider.ts +++ b/example-apps/tanstack-start/src/routes/api/burrito/consider.ts @@ -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 }) }, },