> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt # The wizard's bestiary - Handbook The wizard runs on a harness we own, so we get feedback from every run on how our skills and SDKs behave in real repositories. We turn that feedback into the edge cases below, and feed the fixes back into the SDKs, evals, and docs so general agents benefit too. Below are 645 edge cases the wizard has caught. Each one helped us improve the integration skills. Think of these as the Wizard's battle trophies! ## Setup and init Init runs late, never, or in the wrong file The SDK starts too late, more than once, or in a file that never runs. **Example: Browser init inside a component effect** **Problem.** Older posthog-js docs and most blog posts show a 'use client' PostHogProvider or \_app.tsx that calls posthog.init in useEffect, often with a dynamic import to dodge SSR. Agents reproduce it from training data even when instrumentation-client.ts is available. **Impact.** Init waits for hydration, so the first pageview and captures or flag reads made before the effect runs are dropped or throw. Every caller needs init guards and flags flicker. Same for React SPAs, where init belongs at module scope in main.jsx. Generic ```jsx export function PostHogInitializer() { useEffect(() => { void import('posthog-js').then(({ default: posthog }) => { posthog.init(token, { api_host: host }) window.posthogInitialized = true window.dispatchEvent(new Event('posthog_initialized')) }) }, []) } // pages call posthog.capture() with no init guard ``` Wizard ```typescript // instrumentation-client.ts runs before any component renders import posthog from 'posthog-js' posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, { api_host: '/ingest', defaults: '2026-01-30', }) // components: useFeatureFlagEnabled('flag') works without PostHogProvider ``` **Also catches:** - Fast models write Next.js init from memory into a lib file nothing imports, with a guessed env var and no defaults. The wizard has them follow the example's instrumentation-client.ts. [init.md](https://github.com/PostHog/context-mill/blob/main/context/agents/integration-v2/init.md) - Same for iOS, where setup goes in a SwiftUI onAppear that runs many times and after early events. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - Same for Flutter, where setup is not awaited before runApp, so the first frames send before the SDK is ready. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - Older Next.js silently ignores instrumentation-client.ts, so PostHog never starts. The wizard checks the installed version and refuses Next.js below 15.3. [posthog-best-practices/description.md](https://github.com/PostHog/context-mill/blob/main/context/skills/posthog-best-practices/description.md) - The instrumentation-client file gets a .tsx or .jsx extension, so Next.js may not pick it up. [instrumentation-client.ts](https://github.com/PostHog/context-mill/blob/main/example-apps/next-app-router/instrumentation-client.ts) - App Router providers are added to a Pages Router project, so nothing mounts them. - One init shape is used for every rendering mode or version, like Astro static, SSR, hybrid and view transitions, Nuxt 3.0 to 3.6 against 3.7+, or TanStack file and code routing. [integration/config.yaml](https://github.com/PostHog/context-mill/blob/main/context/skills/integration/config.yaml) - ES module imports run before the entry file's body, so modules that capture on load run before init. The wizard inits in its own module that every file imports. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - The agent creates a provider file but never imports or mounts it, and the build stays green. The wizard checks that every created component is actually used. [3-revise.md](https://github.com/PostHog/context-mill/blob/main/context/skills/integration/references/3-revise.md) - Angular init in ngOnInit runs inside the zone, so replay's DOM observers trigger extra change detection. The wizard runs init outside the Angular zone. - The Phoenix PostHog Plug is skipped or placed after the router, so request data and tracing headers never reach events. The wizard puts the Plug before the router. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - React Router v6 and v7 framework, data and declarative modes each need init in a different entry file. The wizard picks the entry file for the detected mode. [react-router-wizard-agent.ts](https://github.com/PostHog/wizard/blob/main/src/programs/frameworks/react-router/react-router-wizard-agent.ts) - Same for TanStack Router, where a file-based `__root.tsx` is created in a code-based app. [tanstack-router/utils.ts](https://github.com/PostHog/wizard/blob/main/src/programs/frameworks/tanstack-router/utils.ts) - Same for Nuxt, where @posthog/nuxt supports only 3.7+, so 3.0 to 3.6 need a plugin. [integration/config.yaml](https://github.com/PostHog/context-mill/blob/main/context/skills/integration/config.yaml) - Same for Laravel 11+, where middleware and providers moved to bootstrap/app.php. [laravel-wizard-agent.ts](https://github.com/PostHog/wizard/blob/main/src/programs/frameworks/laravel/laravel-wizard-agent.ts) - Same for Next.js projects that use src/, where instrumentation-client.ts belongs beside src/app. - Same for old framework or platform versions, which get current APIs and the latest SDK. [frameworks](https://github.com/PostHog/wizard/tree/main/src/programs/frameworks) - Android setup in MainActivity reruns on every Activity and misses early events. The wizard sets up once in the Application class. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - Kotlin Multiplatform setup copies the native one-argument shape and fails on Android. The wizard passes the right PostHogContext on each platform. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - Astro 5 prerenders API routes by default, so server-side capture never runs, or the agent flips the whole site to SSR. The wizard opts out only the routes that capture. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) Browser-only code runs on the server Code that needs a browser runs during server rendering, or server code ends up in the browser. **Example: Nuxt plugin also runs on the server** **Problem.** plugins/posthog.ts looks client-side, but Nuxt runs unsuffixed plugins in Nitro SSR too. Only the .client filename makes it browser-only. **Impact.** SSR throws 'window is not defined' or initializes a browser SDK on the server. Generic ```typescript // plugins/posthog.ts (runs on server and client) export default defineNuxtPlugin(() => { posthog.init(runtimeConfig.public.posthog.publicKey, { tracing_headers: [window.location.hostname], }) }) ``` Wizard ```typescript // plugins/posthog.client.ts (client-only by filename) export default defineNuxtPlugin((nuxtApp) => { const posthogClient = posthog.init(runtimeConfig.public.posthog.publicKey, { api_host: runtimeConfig.public.posthog.host, tracing_headers: [window.location.hostname], }) return { provide: { posthog: posthogClient as PostHog } } }) ``` **Also catches:** - Same for provider options like tracing\_headers that read window in a root shell that also renders on the server. [\_\_root.tsx](https://github.com/PostHog/context-mill/blob/main/example-apps/tanstack-start/src/routes/__root.tsx) - Same for SvelteKit, where +layout.ts and the +layout.svelte module script also run on the server. [hooks.client.ts](https://github.com/PostHog/context-mill/blob/main/example-apps/sveltekit/src/hooks.client.ts) - Same for Angular, where constructors, ngOnInit and app config also run during SSR. The fix is one DI service with a platform check. [posthog.service.ts](https://github.com/PostHog/context-mill/blob/main/example-apps/angular/src/app/services/posthog.service.ts) - Same for Nuxt components that call the client with no guard, when there is no client during server render or no token. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - A minimal init without the defaults date injects lazy scripts into the body and causes hydration errors. The wizard sets defaults so scripts go into the head. [entry.client.tsx](https://github.com/PostHog/context-mill/blob/main/example-apps/react-react-router-7-framework/app/entry.client.tsx) - posthog-js is imported into server routes, where it has no window and sends nothing. The wizard uses posthog-node for server code. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - The reverse. A posthog-node helper is imported into a use client file and ends up in the browser bundle. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - posthog-js init options are passed to the posthog-node client, which has a different options type. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - The posthog-js capture(event, props) shape is used in posthog-node, which takes one object with a required distinctId. [todo.js](https://github.com/PostHog/context-mill/blob/main/example-apps/javascript-node/todo.js) - The agent adds 'use client' or 'use server' to make a capture compile, which changes the file's rendering boundary. The wizard keeps the existing client and server split. [commandments.ts](https://github.com/PostHog/wizard/blob/main/src/agent/commandments.ts) - An Astro, SvelteKit or React Router app on a Cloudflare adapter looks like Node, so Node-shaped code drops events on Workers. The wizard detects the Workers target from the adapter or wrangler config. [cloudflare-detection.ts](https://github.com/PostHog/wizard/blob/main/src/programs/cloudflare-detection.ts) - Same for process.env on Workers, which works only with nodejs\_compat and, in Nuxt, only inside handlers. [docs.yaml](https://github.com/PostHog/context-mill/blob/main/context/docs.yaml) The same thing set up twice The agent adds a second copy of something the project or an earlier step already set up. **Example: Second PostHog init beside the existing one** **Problem.** Agents add a PostHogProvider or init by reflex without searching for an existing provider or instrumentation-client.ts init, because most React examples pair init with a provider, including for flag hooks. Double init throws no error. **Impact.** Two browser inits race to stamp $set\_once properties like $initial\_pathname, corrupting attribution and double-capturing pageviews. One multi-SDK customer saw about 165k blocked merges per day. **Variants.** - A second browser provider or init next to instrumentation-client.ts. - A new client built just for a feature like metrics or AI. - A migration that swaps an old vendor provider for a second PostHog one. Generic ```jsx // instrumentation-client.ts posthog.init(KEY, { api_host: '/ingest' }) // app/providers.tsx (always mounted) useEffect(() => { posthog.init(KEY, { api_host: '/ingest' }) }, []) // app/layout.tsx: {children} // and added again in the providers list ``` Wizard ```jsx // instrumentation-client.ts: the one browser init //IMPORTANT: Never combine this approach with other client-side PostHog // initialization approaches, especially components like a PostHogProvider. posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, { api_host: '/ingest' }) // components: hooks use the singleton, no provider needed const on = useFeatureFlagEnabled('promo') // audit init-not-duplicated: warning on 2+ browser inits, error if confirmed on one page ``` **Also catches:** - Same for Flutter, where native auto-init from AndroidManifest.xml and Info.plist stays on beside the Dart setup. [flutter-wizard-agent.ts](https://github.com/PostHog/wizard/blob/main/src/programs/frameworks/flutter/flutter-wizard-agent.ts) - The agent adds a PostHog client on OpenTelemetry paths that need none, or stacks a wrapper and an instrumentor on one provider. The wizard builds a client only on wrapper and manual paths and reuses an existing one. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - A later task cannot see what earlier tasks did, so it adds identify or reset again. The wizard passes earlier steps' handoffs to every later task. [agent-prompt-loader.ts](https://github.com/PostHog/wizard/blob/main/src/agent/agent-prompt-loader.ts) - The agent adds alias() next to identify() to be safe, which can block the identify merge. The wizard relies on identify() and keeps alias() for narrow cases. [3-identify-lifecycle.md](https://github.com/PostHog/context-mill/blob/main/context/skills/audit-identify/references/3-identify-lifecycle.md) - The same milestone is captured in the browser and the API, so every conversion counts twice. The wizard captures key milestones once, on the backend. [product-analytics.md](https://github.com/PostHog/context-mill/blob/main/context/skills/posthog-best-practices/references/product-analytics.md) - A manual pageview component is added while defaults already capture history\_change pageviews, so every navigation counts twice. The wizard keeps the automatic pageviews only. [capture/description.md](https://github.com/PostHog/context-mill/blob/main/context/skills/integration-v2/capture/description.md) - Custom page\_viewed events are added on each screen mount, on top of automatic pageviews. [instrument-product-analytics/description.md](https://github.com/PostHog/context-mill/blob/main/context/skills/omnibus/instrument-product-analytics/description.md) - The full setup flow runs on a project that already has PostHog. The wizard detects the existing install and offers other programs first. [intro-menu.ts](https://github.com/PostHog/wizard/blob/main/src/tui/programs/posthog-integration/intro-menu.ts) Project misread: wrong platform, mode, or half of the stack The agent misreads what kind of project it is in and sets up the wrong SDK or only half the app. **Example: Flutter repo misread as a native Android or iOS app** **Problem.** A Flutter repo has real Gradle, AndroidManifest, MainActivity.kt and Xcode files under android/ and ios/, so the first matching native or Java detector claims it. **Impact.** posthog-android or the native source-map flow is wired into the shell. Dart screens, events and the web target are never tracked, and Java server projects can get claimed as Android. Generic ```kotlin // android/app/src/main/kotlin/.../MainActivity.kt inside a Flutter app class MainActivity : FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) PostHogAndroid.setup(application, PostHogAndroidConfig(KEY, HOST)) } // Dart code untouched } ``` Wizard ```typescript // constants.ts: flutter is ordered before kmp/swift/android; java comes after them flutter = 'flutter', // flutter detect: anchored to a key/value line, so '# sdk: flutter' and pure Dart don't match /^\s*sdk:\s*flutter\s*$/m.test(pubspec ?? '') // android + java detect(): // pubspec.yaml means Flutter - its android/ subtree would match below. if (fs.existsSync(path.join(installDir, 'pubspec.yaml'))) return false; // source maps: VARIANT_PRECEDENCE = ['react-native', 'flutter', 'android', 'ios', ...] ``` **Also catches:** - A full-stack app gets only the browser SDK, so server events and errors are lost. The wizard sets up every runtime the app has. [runtime-notes.ts](https://github.com/PostHog/wizard/blob/main/src/agent/runner/harness/pi/runtime-notes.ts) - A Gemfile or requirements.txt that only holds tooling is read as a server app, so release linking edits the wrong stack. The wizard ignores manifests that only carry tooling. [setup-error-tracking.md](https://github.com/PostHog/context-mill/blob/main/context/agents/error-tracking/setup-error-tracking.md) - A React Native or Flutter repo has ios/ and Gradle files, so it gets the native dSYM uploader instead of JS source maps. [setup-error-tracking.md](https://github.com/PostHog/context-mill/blob/main/context/agents/error-tracking/setup-error-tracking.md) - A pure Dart package with a pubspec.yaml gets a symbol upload, but it never builds an app. [detect-agentic.ts](https://github.com/PostHog/wizard/blob/main/src/programs/error-tracking-upload-source-maps/detect-agentic.ts) - A go.mod replace, comment or module line is read as a posthog-go dependency. [detect-agentic.ts](https://github.com/PostHog/wizard/blob/main/src/programs/error-tracking-upload-source-maps/detect-agentic.ts) - Upload is wired for stacks that cannot use it, like apps with no SDK or no build command. [detect-agentic.ts](https://github.com/PostHog/wizard/blob/main/src/programs/error-tracking-upload-source-maps/detect-agentic.ts) - A Kotlin Multiplatform app gets the native Android and iOS SDKs instead of posthog-kmp. The wizard adds posthog-kmp to commonMain and sets it up once in shared code. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) - Expo and bare React Native get the same recipe, so env access and native steps break. The wizard uses separate steps for Expo and bare React Native. [utils.ts](https://github.com/PostHog/wizard/blob/main/src/programs/frameworks/react-native/utils.ts) - An API-only Rails app gets a browser snippet and even a new layout file. The wizard detects config.api\_only and sets up the backend only. [utils.ts](https://github.com/PostHog/wizard/blob/main/src/programs/frameworks/rails/utils.ts) - Same for Replay vision, set up on an API with no pages or a KMP project with no replay support. [replay-vision/index.ts](https://github.com/PostHog/wizard/blob/main/src/programs/replay-vision/index.ts) - Same for browser apps, where the generic Node fallback matched any package.json before the web detector. [constants.ts](https://github.com/PostHog/wizard/blob/main/src/shared/constants.ts) - Same for Gradle, where build.gradle alone does not say Android, KMP or a JVM server. [java-wizard-agent.ts](https://github.com/PostHog/wizard/blob/main/src/programs/frameworks/java/java-wizard-agent.ts) - Same for a substring check on django or flask, which matches comments and plugin names. [django-wizard-agent.ts](https://github.com/PostHog/wizard/blob/main/src/programs/frameworks/django/django-wizard-agent.ts) - Same for Go or Rust repos, where a lint or husky package.json matches the JS fallback first. [constants.ts](https://github.com/PostHog/wizard/blob/main/src/shared/constants.ts) - Detection listed only some manifests, so Flutter and Elixir projects matched nothing and the agent improvised. The wizard checks every supported manifest and lockfile. [instrument-integration/description.md](https://github.com/PostHog/context-mill/blob/main/context/skills/omnibus/instrument-integration/description.md) - Same for existing installs, where SPM, CocoaPods, Gradle, NuGet, mix and Cargo installs look like no PostHog. [self-driving/detect.ts](https://github.com/PostHog/wizard/blob/main/src/programs/self-driving/detect.ts) - Same for XcodeGen apps, which commit project.yml instead of a .xcodeproj. [swift-wizard-agent.ts](https://github.com/PostHog/wizard/blob/main/src/programs/frameworks/swift/swift-wizard-agent.ts) - Same for LLM detection, which reads package.json and never requirements.txt or pyproject. [features.ts](https://github.com/PostHog/wizard/blob/main/src/programs/detection/features.ts) - The framework version is read from the package.json range, not the installed package, so version gates refuse valid projects. The wizard reads the version from node\_modules. [package-json.ts](https://github.com/PostHog/wizard/blob/main/src/shared/utils/package-json.ts) - Full-stack apps get the browser metrics SDK, so server work goes unmeasured. The wizard picks the server metrics variant for full-stack apps. [verify-sdk.md](https://github.com/PostHog/context-mill/blob/main/context/agents/metrics/verify-sdk.md) - Same for logs, where a server OTLP exporter is added to a client-only app. [logs.md](https://github.com/PostHog/context-mill/blob/main/context/agents/integration-v2/logs.md) - A Rails or Laravel app gets only the server SDK, so views send no pageviews, replay or client errors. The wizard also adds the browser snippet to the layout. [commandments.yaml](https://github.com/PostHog/context-mill/blob/main/context/commandments.yaml) Framework conventions ignored or written from stale memory The agent writes code from old training data instead of following the framework's current conventions. **Example: Svelte 4 syntax written into a runes app** **Problem.** Training data is mostly Svelte 4, so agents write export let and on:click, and a JS build may not catch rune misuse, so the agent finishes without Svelte's own validator. **Impact.** Components build but misbehave under runes, or the compiler rejects them. Generic ```html